Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-01 18:59:23 +04:00
parent 950dc41662
commit dfa6798f07
16 changed files with 220 additions and 82 deletions

View file

@ -135,7 +135,7 @@ internal class TesterActivity : ComposeActivity() {
composable(route = TesterScreen.BLOCKCHAIN_PROVIDERS.name) {
val viewModel = hiltViewModel<BlockchainProvidersViewModel>().apply {
setupNavigation(innerTesterRouter)
setupNavigation(innerTesterRouter, appFinisher)
}
val state by viewModel.state.collectAsStateWithLifecycle()

View file

@ -10,6 +10,7 @@ internal data class BlockchainProvidersUM(
val topBar: TopBarWithRefreshUM,
val searchBar: SearchBarUM,
val blockchainProviders: ImmutableList<ProvidersUM>,
val onRestartAppClick: () -> Unit,
) {
@Immutable

View file

@ -30,6 +30,7 @@ import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.blockchain.common.network.providers.ProviderType
import com.tangem.core.ui.components.AdditionalTextInputDialogUM
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.TextInputDialog
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.fields.entity.SearchBarUM
@ -59,7 +60,7 @@ internal fun BlockchainProvidersScreen(state: BlockchainProvidersUM) {
.fillMaxSize()
.background(TangemTheme.colors.background.primary),
) {
LazyColumn {
LazyColumn(contentPadding = PaddingValues(bottom = 64.dp)) {
stickyHeader {
Header(topBar = state.topBar, searchBar = state.searchBar)
}
@ -85,6 +86,15 @@ internal fun BlockchainProvidersScreen(state: BlockchainProvidersUM) {
itemContent = { BlockchainProvidersItem(it) },
)
}
PrimaryButton(
text = stringResourceSafe(id = R.string.restart_app),
onClick = state.onRestartAppClick,
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing16),
)
}
}
@ -427,6 +437,7 @@ private fun Preview_BlockchainProvidersScreen() {
),
),
),
onRestartAppClick = {},
),
)
}

View file

@ -7,6 +7,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.network.providers.ProviderType
import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager
import com.tangem.blockchainsdk.providers.MutableBlockchainProvidersTypesManager
import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.feature.tester.impl.R
@ -46,6 +47,7 @@ internal class BlockchainProvidersViewModel @Inject constructor(
onActiveChange = {},
),
blockchainProviders = persistentListOf(),
onRestartAppClick = {},
),
)
@ -57,10 +59,11 @@ internal class BlockchainProvidersViewModel @Inject constructor(
subscribeOnBlockchainProviderTypesUpdates()
}
fun setupNavigation(router: InnerTesterRouter) {
fun setupNavigation(router: InnerTesterRouter, appFinisher: AppFinisher) {
_state.update { state ->
state.copy(
topBar = state.topBar.copy(onBackClick = router::back),
onRestartAppClick = appFinisher::restart,
)
}
}
@ -116,7 +119,7 @@ internal class BlockchainProvidersViewModel @Inject constructor(
private fun onRefreshClick() {
viewModelScope.launch {
manager.update()
manager.recoverInitialState()
}
}

View file

@ -4,6 +4,7 @@ plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
@ -24,7 +25,7 @@ dependencies {
// endregion
// region DI libraries
implementation(deps.hilt.core)
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
// endregion

View file

@ -1,39 +1,43 @@
package com.tangem.blockchainsdk.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.network.providers.ProviderType
import com.tangem.blockchainsdk.BlockchainProvidersResponse
import com.tangem.blockchainsdk.providers.BlockchainProviderTypes
import com.tangem.blockchainsdk.utils.createPrivateProviderType
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.datasource.local.config.providers.models.ProviderModel
import com.tangem.utils.converter.Converter
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.utils.converter.TwoWayConverter
import timber.log.Timber
/**
* Converts [BlockchainProvidersResponse] to [BlockchainProviderTypes]
* Converts [BlockchainProvidersResponse] to [BlockchainProviderTypes] and vice versa
*
[REDACTED_AUTHOR]
*/
internal object BlockchainProviderTypesConverter :
Converter<BlockchainProvidersResponse, BlockchainProviderTypes> {
TwoWayConverter<BlockchainProvidersResponse, BlockchainProviderTypes> {
override fun convert(value: BlockchainProvidersResponse): BlockchainProviderTypes {
return value.mapNotNull { (networkId, blockchainProviders) ->
val blockchain = Blockchain.fromNetworkId(networkId) ?: return@mapNotNull null
val providerTypes = blockchainProviders.mapNotNull { provider ->
when (provider) {
is ProviderModel.Public -> ProviderType.Public(url = provider.url)
is ProviderModel.Private -> createPrivateProviderType(name = provider.name)
ProviderModel.UnsupportedType -> {
Timber.e("$blockchain provider type is not supported")
null
}
}
val providerTypes = ProviderTypeConverter.convertList(input = blockchainProviders)
providerTypes.forEach {
if (it == null) Timber.e("$blockchain provider type is not supported")
}
blockchain to providerTypes
blockchain to providerTypes.filterNotNull()
}
.toMap()
}
override fun convertBack(value: BlockchainProviderTypes): BlockchainProvidersResponse {
return value.mapNotNull { (blockchain, providerTypes) ->
val networkId = blockchain.toNetworkId()
val blockchainProviders = ProviderTypeConverter.convertListBack(input = providerTypes)
networkId to blockchainProviders
}
.toMap()
}

View file

@ -0,0 +1,35 @@
package com.tangem.blockchainsdk.converters
import com.tangem.blockchain.common.network.providers.ProviderType
import com.tangem.blockchainsdk.providers.ProviderTypeIdMapping
import com.tangem.datasource.local.config.providers.models.ProviderModel
import com.tangem.utils.converter.TwoWayConverter
/**
* Converts [ProviderModel] to [ProviderType] and vice versa
*
[REDACTED_AUTHOR]
*/
internal object ProviderTypeConverter : TwoWayConverter<ProviderModel, ProviderType?> {
override fun convert(value: ProviderModel): ProviderType? {
return when (value) {
is ProviderModel.Public -> ProviderType.Public(url = value.url)
is ProviderModel.Private -> ProviderTypeIdMapping.entries.firstOrNull { it.id == value.name }?.providerType
ProviderModel.UnsupportedType -> null
}
}
override fun convertBack(value: ProviderType?): ProviderModel {
return when (value) {
null -> ProviderModel.UnsupportedType
is ProviderType.Public -> ProviderModel.Public(url = value.url)
else -> {
val id = ProviderTypeIdMapping.entries.firstOrNull { it.providerType == value }?.id
?: return ProviderModel.UnsupportedType
ProviderModel.Private(id)
}
}
}
}

View file

@ -1,17 +1,26 @@
package com.tangem.blockchainsdk.di
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.blockchainsdk.BlockchainProvidersResponse
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.blockchainsdk.DefaultBlockchainSDKFactory
import com.tangem.blockchainsdk.WalletManagerFactoryCreator
import com.tangem.blockchainsdk.accountcreator.DefaultAccountCreator
import com.tangem.blockchainsdk.datastorage.DefaultBlockchainDataStorage
import com.tangem.blockchainsdk.featuretoggles.DefaultBlockchainSDKFeatureToggles
import com.tangem.blockchainsdk.providers.BlockchainProviderTypesStore
import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager
import com.tangem.blockchainsdk.providers.DevBlockchainProvidersTypesManager
import com.tangem.blockchainsdk.providers.ProdBlockchainProvidersTypesManager
import com.tangem.blockchainsdk.providers.dev.BlockchainProvidersResponseSerializer
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.libs.blockchain_sdk.BuildConfig
@ -19,7 +28,10 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@ -45,16 +57,35 @@ internal object BlockchainSDKFactoryModule {
@Provides
@Singleton
fun provideBlockchainProvidersTypesManager(
devBlockchainProvidersTypesManager: DevBlockchainProvidersTypesManager,
prodBlockchainProvidersTypesManager: ProdBlockchainProvidersTypesManager,
blockchainProviderTypesStore: BlockchainProviderTypesStore,
changedBlockchainProvidersStore: DataStore<BlockchainProvidersResponse>,
): BlockchainProvidersTypesManager {
return if (BuildConfig.DEBUG) {
devBlockchainProvidersTypesManager
return if (BuildConfig.TESTER_MENU_ENABLED) {
DevBlockchainProvidersTypesManager(
prodBlockchainProvidersTypesManager = prodBlockchainProvidersTypesManager,
blockchainProviderTypesStore = blockchainProviderTypesStore,
changedBlockchainProvidersStore = changedBlockchainProvidersStore,
)
} else {
prodBlockchainProvidersTypesManager
}
}
@Provides
@Singleton
fun provideChangedBlockchainProvidersResponseDataStore(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): DataStore<BlockchainProvidersResponse> {
return DataStoreFactory.create(
serializer = BlockchainProvidersResponseSerializer(moshi),
produceFile = { context.dataStoreFile("changed_providers") },
scope = CoroutineScope(dispatchers.io + SupervisorJob()),
)
}
@Provides
@Singleton
fun provideWalletManagerFactoryCreator(

View file

@ -3,7 +3,6 @@ package com.tangem.blockchainsdk.providers
import androidx.core.util.PatternsCompat
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchainsdk.BlockchainProvidersResponse
import com.tangem.blockchainsdk.utils.createPrivateProviderType
import com.tangem.datasource.local.config.providers.models.ProviderModel
import timber.log.Timber
@ -48,11 +47,11 @@ internal object BlockchainProvidersResponseMerger {
return result.guaranteeUrlsEndWithSlash()
}
private fun List<ProviderModel>.filterUnsupportedProviders() = filter {
val isSupportedType = it !is ProviderModel.UnsupportedType
private fun List<ProviderModel>.filterUnsupportedProviders() = filter { model ->
val isSupportedType = model !is ProviderModel.UnsupportedType
val isSupportedPrivateType = if (it is ProviderModel.Private) {
createPrivateProviderType(it.name) != null
val isSupportedPrivateType = if (model is ProviderModel.Private) {
ProviderTypeIdMapping.entries.any { it.id == model.name }
} else {
true
}

View file

@ -1,11 +1,11 @@
package com.tangem.blockchainsdk.providers
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.Flow
/** Blockchain providers types manager */
interface BlockchainProvidersTypesManager {
fun get(): StateFlow<BlockchainProviderTypes>
fun get(): Flow<BlockchainProviderTypes>
suspend fun update()
}

View file

@ -1,45 +1,68 @@
package com.tangem.blockchainsdk.providers
import androidx.datastore.core.DataStore
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.network.providers.ProviderType
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.properties.Delegates.notNull
import com.tangem.blockchainsdk.BlockchainProvidersResponse
import com.tangem.blockchainsdk.converters.BlockchainProviderTypesConverter
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import timber.log.Timber
/**
* Implementation of [BlockchainProvidersTypesManager] in DEV environment
*
* @property prodBlockchainProvidersTypesManager prod manager
* @property blockchainProviderTypesStore blockchain provider types store
* @property changedBlockchainProvidersStore changed blockchain providers store
*
[REDACTED_AUTHOR]
*/
@Singleton
internal class DevBlockchainProvidersTypesManager @Inject constructor(
internal class DevBlockchainProvidersTypesManager(
private val prodBlockchainProvidersTypesManager: ProdBlockchainProvidersTypesManager,
private val blockchainProviderTypesStore: BlockchainProviderTypesStore,
) : BlockchainProvidersTypesManager by prodBlockchainProvidersTypesManager,
MutableBlockchainProvidersTypesManager {
private val changedBlockchainProvidersStore: DataStore<BlockchainProvidersResponse>,
) : MutableBlockchainProvidersTypesManager {
private var primaryResponse: BlockchainProviderTypes by notNull()
override fun get(): Flow<BlockchainProviderTypes> = changedBlockchainProvidersStore.data
.map(BlockchainProviderTypesConverter::convert)
override suspend fun update() {
prodBlockchainProvidersTypesManager.update()
primaryResponse = blockchainProviderTypesStore.get().value
val initial = blockchainProviderTypesStore.get().value
val changed = changedBlockchainProvidersStore.data.firstOrNull().orEmpty()
if (changed.isEmpty()) {
Timber.i("Initialize ChangedBlockchainProvidersStore")
changedBlockchainProvidersStore.updateData {
BlockchainProviderTypesConverter.convertBack(initial)
}
}
}
override suspend fun recoverInitialState() {
val initial = blockchainProviderTypesStore.get().value
changedBlockchainProvidersStore.updateData {
BlockchainProviderTypesConverter.convertBack(initial)
}
}
override suspend fun update(blockchain: Blockchain, providers: List<ProviderType>) {
val currentTypes = blockchainProviderTypesStore.get().value
changedBlockchainProvidersStore.updateData {
val providerModels = BlockchainProviderTypesConverter.convertBack(value = mapOf(blockchain to providers))
val updatedTypes = currentTypes + (blockchain to providers)
blockchainProviderTypesStore.store(value = updatedTypes)
it + providerModels
}
}
override suspend fun isMatchWithMerged(): Boolean {
val current = blockchainProviderTypesStore.get().value
val initial = blockchainProviderTypesStore.get().value
val changed = changedBlockchainProvidersStore.data.firstOrNull().orEmpty()
return current == primaryResponse
return initial == BlockchainProviderTypesConverter.convert(changed)
}
}

View file

@ -13,6 +13,9 @@ interface MutableBlockchainProvidersTypesManager : BlockchainProvidersTypesManag
/** Update [providers] for [blockchain] */
suspend fun update(blockchain: Blockchain, providers: List<ProviderType>)
/** Recover initial state */
suspend fun recoverInitialState()
/** Checks that current [BlockchainProviderTypes] was changed */
suspend fun isMatchWithMerged(): Boolean
}

View file

@ -1,7 +1,7 @@
package com.tangem.blockchainsdk.providers
import com.tangem.blockchainsdk.converters.BlockchainProviderTypesConverter
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.Flow
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -20,7 +20,7 @@ internal class ProdBlockchainProvidersTypesManager @Inject constructor(
private val blockchainProviderTypesStore: BlockchainProviderTypesStore,
) : BlockchainProvidersTypesManager {
override fun get(): StateFlow<BlockchainProviderTypes> = blockchainProviderTypesStore.get()
override fun get(): Flow<BlockchainProviderTypes> = blockchainProviderTypesStore.get()
override suspend fun update() {
val response = blockchainProvidersResponseLoader.load()

View file

@ -0,0 +1,27 @@
package com.tangem.blockchainsdk.providers
import com.tangem.blockchain.common.network.providers.ProviderType
/** Mapping of [providerType] with [id] */
internal enum class ProviderTypeIdMapping(val id: String, val providerType: ProviderType) {
NowNodes(id = "nownodes", providerType = ProviderType.NowNodes),
GetBlock(id = "getblock", providerType = ProviderType.GetBlock),
QuickNode(id = "quicknode", providerType = ProviderType.QuickNode),
BitcoinBlockchair(id = "blockchair", providerType = ProviderType.BitcoinLike.Blockchair),
BitcoinBlockcypher(id = "blockcypher", providerType = ProviderType.BitcoinLike.Blockcypher),
CardanoAdalite(id = "adalite", providerType = ProviderType.Cardano.Adalite),
CardanoRosetta(id = "tangemRosetta", providerType = ProviderType.Cardano.Rosetta),
ChiaFireAcademy(id = "fireAcademy", providerType = ProviderType.Chia.FireAcademy),
ChiaTangem(id = "tangemChia", providerType = ProviderType.Chia.Tangem),
ChiaTangemNew(id = "tangemChia3", providerType = ProviderType.Chia.TangemNew),
EthereumInfura(id = "infura", providerType = ProviderType.EthereumLike.Infura),
HederaArkhia(id = "arkhiaHedera", providerType = ProviderType.Hedera.Arkhia),
KaspaSecondary(id = "kaspa", providerType = ProviderType.Kaspa.SecondaryAPI),
SolanaOfficial(id = "solana", providerType = ProviderType.Solana.Official),
TonCentral(id = "ton", providerType = ProviderType.Ton.TonCentral),
TronGrid(id = "tron", providerType = ProviderType.Tron.TronGrid),
BittensorDwellir(id = "dwellirBittensor", providerType = ProviderType.Bittensor.Dwellir),
BittensorOnfinality(id = "onfinalityBittensor", providerType = ProviderType.Bittensor.Onfinality),
KoinosPro(id = "koinospro", providerType = ProviderType.Koinos.KoinosPro),
;
}

View file

@ -0,0 +1,34 @@
package com.tangem.blockchainsdk.providers.dev
import androidx.datastore.core.Serializer
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.blockchainsdk.BlockchainProvidersResponse
import com.tangem.datasource.local.config.providers.models.ProviderModel
import java.io.InputStream
import java.io.OutputStream
/** DataStore serializer for [BlockchainProvidersResponse]. Implemented by [Moshi]. */
internal class BlockchainProvidersResponseSerializer(moshi: Moshi) : Serializer<BlockchainProvidersResponse> {
private val adapter by lazy {
val providersType = Types.newParameterizedType(List::class.java, ProviderModel::class.java)
val type = Types.newParameterizedType(Map::class.java, String::class.java, providersType)
moshi.adapter<BlockchainProvidersResponse>(type)
}
override val defaultValue: BlockchainProvidersResponse = emptyMap()
override suspend fun readFrom(input: InputStream): BlockchainProvidersResponse {
return input.bufferedReader().use { reader ->
adapter.fromJson(reader.readText()) ?: defaultValue
}
}
override suspend fun writeTo(t: BlockchainProvidersResponse, output: OutputStream) {
output.bufferedWriter().use { writer ->
writer.write(adapter.toJson(t))
}
}
}

View file

@ -1,34 +0,0 @@
package com.tangem.blockchainsdk.utils
import com.tangem.blockchain.common.network.providers.ProviderType
import timber.log.Timber
/** Create private provider by [name] or return null */
@Suppress("CyclomaticComplexMethod")
fun createPrivateProviderType(name: String): ProviderType? {
return when (name) {
"blockchair" -> ProviderType.BitcoinLike.Blockchair
"blockcypher" -> ProviderType.BitcoinLike.Blockcypher
"adalite" -> ProviderType.Cardano.Adalite
"tangemRosetta" -> ProviderType.Cardano.Rosetta
"fireAcademy" -> ProviderType.Chia.FireAcademy
"tangemChia" -> ProviderType.Chia.Tangem
"tangemChia3" -> ProviderType.Chia.TangemNew
"infura" -> ProviderType.EthereumLike.Infura
"getblock" -> ProviderType.GetBlock
"arkhiaHedera" -> ProviderType.Hedera.Arkhia
"kaspa" -> ProviderType.Kaspa.SecondaryAPI
"nownodes" -> ProviderType.NowNodes
"quicknode" -> ProviderType.QuickNode
"solana" -> ProviderType.Solana.Official
"ton" -> ProviderType.Ton.TonCentral
"tron" -> ProviderType.Tron.TronGrid
"dwellirBittensor" -> ProviderType.Bittensor.Dwellir
"onfinalityBittensor" -> ProviderType.Bittensor.Onfinality
"koinospro" -> ProviderType.Koinos.KoinosPro
else -> {
Timber.e("Private provider with name $name is not supported")
null
}
}
}