Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-05 15:42:16 +03:00
parent 820578708c
commit 83d291d7d0
8 changed files with 422 additions and 323 deletions

View file

@ -1,9 +1,7 @@
package com.tangem.blockchainsdk
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchainsdk.converters.BlockchainProviderTypesConverter
import com.tangem.blockchainsdk.providers.BlockchainProviderTypesStore
import com.tangem.blockchainsdk.providers.BlockchainProvidersResponseLoader
import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.providers.models.ProviderModel
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -11,24 +9,22 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
internal typealias BlockchainProvidersResponse = Map<String, List<ProviderModel>>
/**
* Implementation of Blockchain SDK components factory
*
* @property blockchainProvidersResponseLoader blockchain providers response loader
* @property environmentConfigStorage environment config storage
* @property blockchainProviderTypesStore blockchain provider types store
* @property walletManagerFactoryCreator wallet manager factory creator
* @property blockchainProvidersTypesManager blockchain providers types manager
* @property environmentConfigStorage environment config storage
* @property walletManagerFactoryCreator wallet manager factory creator
* @param dispatchers coroutine dispatchers provider
*
[REDACTED_AUTHOR]
*/
internal class DefaultBlockchainSDKFactory(
private val blockchainProvidersResponseLoader: BlockchainProvidersResponseLoader,
private val blockchainProvidersTypesManager: BlockchainProvidersTypesManager,
private val environmentConfigStorage: EnvironmentConfigStorage,
private val blockchainProviderTypesStore: BlockchainProviderTypesStore,
private val walletManagerFactoryCreator: WalletManagerFactoryCreator,
dispatchers: CoroutineDispatcherProvider,
) : BlockchainSDKFactory {
@ -39,7 +35,7 @@ internal class DefaultBlockchainSDKFactory(
override suspend fun init() {
coroutineScope {
updateBlockchainProviderTypes()
launch { blockchainProvidersTypesManager.update() }
}
}
@ -48,28 +44,11 @@ internal class DefaultBlockchainSDKFactory(
private fun createWalletManagerFactory(): Flow<WalletManagerFactory?> {
return combine(
flow = environmentConfigStorage.getConfig().map { it.blockchainSdkConfig },
flow2 = blockchainProviderTypesStore.get(),
flow2 = blockchainProvidersTypesManager.get(),
// flow3 = subscribe on feature toggles changes, TODO: [REDACTED_JIRA]
transform = walletManagerFactoryCreator::create,
)
// don't use Lazily because some features (WC) require initialized factory on app started
.stateIn(scope = mainScope, started = SharingStarted.Eagerly, initialValue = null)
}
private fun CoroutineScope.updateBlockchainProviderTypes() {
launch {
val response = blockchainProvidersResponseLoader.load()
if (response == null) {
Timber.e("Error loading BlockchainProviderTypes")
return@launch
}
Timber.i("Update BlockchainProviderTypes")
blockchainProviderTypesStore.store(
value = BlockchainProviderTypesConverter.convert(response),
)
}
}
}

View file

@ -7,8 +7,8 @@ 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.BlockchainProvidersResponseLoader
import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager
import com.tangem.blockchainsdk.providers.ProdBlockchainProvidersTypesManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
@ -27,21 +27,28 @@ internal object BlockchainSDKFactoryModule {
@Provides
@Singleton
fun provideBlockchainSDKFactory(
blockchainProvidersResponseLoader: BlockchainProvidersResponseLoader,
blockchainProvidersTypesManager: BlockchainProvidersTypesManager,
environmentConfigStorage: EnvironmentConfigStorage,
walletManagerFactoryCreator: WalletManagerFactoryCreator,
blockchainProviderTypesStore: BlockchainProviderTypesStore,
dispatchers: CoroutineDispatcherProvider,
): BlockchainSDKFactory {
return DefaultBlockchainSDKFactory(
blockchainProvidersResponseLoader = blockchainProvidersResponseLoader,
blockchainProvidersTypesManager = blockchainProvidersTypesManager,
environmentConfigStorage = environmentConfigStorage,
blockchainProviderTypesStore = blockchainProviderTypesStore,
walletManagerFactoryCreator = walletManagerFactoryCreator,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideBlockchainProvidersTypesManager(
prodBlockchainProvidersTypesManager: ProdBlockchainProvidersTypesManager,
): BlockchainProvidersTypesManager {
// In the future, we will use different types of providers for different environments
return prodBlockchainProvidersTypesManager
}
@Provides
@Singleton
fun provideWalletManagerFactoryCreator(

View file

@ -1,12 +1,8 @@
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.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage
import com.tangem.datasource.local.config.providers.models.ProviderModel
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import timber.log.Timber
@ -16,7 +12,7 @@ import javax.inject.Singleton
/**
* Loader of [BlockchainProvidersResponse]
*
* @property tangemTechServiceApi tangem tech api
* @property tangemTechApi tangem tech api
* @property blockchainProvidersStorage blockchain providers storage
* @property dispatchers dispatchers
*
@ -24,120 +20,33 @@ import javax.inject.Singleton
*/
@Singleton
internal class BlockchainProvidersResponseLoader @Inject constructor(
private val tangemTechServiceApi: TangemTechApi,
private val tangemTechApi: TangemTechApi,
private val blockchainProvidersStorage: BlockchainProvidersStorage,
private val dispatchers: CoroutineDispatcherProvider,
) {
private val firebaseCrashlytics by lazy(FirebaseCrashlytics::getInstance)
/** Load [BlockchainProvidersResponse] */
suspend fun load(): BlockchainProvidersResponse? {
val localResponse = loadLocal().ifEmpty { return null }
return runCatching(dispatcher = dispatchers.io, block = ::loadRemote)
.fold(
onSuccess = { remoteResponse -> mergeResponses(local = localResponse, remote = remoteResponse) },
onFailure = {
Timber.e(it, "Failed to load blockchain provider types from backend")
localResponse
},
)
return loadRemote().fold(
onSuccess = { remoteResponse ->
BlockchainProvidersResponseMerger.merge(
local = localResponse,
remote = remoteResponse,
)
},
onFailure = {
Timber.e(it, "Failed to load blockchain provider types from backend")
localResponse
},
)
}
private suspend fun loadLocal(): BlockchainProvidersResponse = blockchainProvidersStorage.getConfigSync()
private suspend fun loadRemote() = tangemTechServiceApi.getBlockchainProviders()
/** Merge blockchains with non-empty providers [remote] from remote with blockchains from local [local] */
private fun mergeResponses(
local: BlockchainProvidersResponse,
remote: BlockchainProvidersResponse,
): BlockchainProvidersResponse {
/*
* Example:
* val remote = mapOf("a" to 1, "b" to 2, "c" to 3)
* val local = mapOf("a" to 11, "e" to 4, "f" to 5)
*
* local + remote // { a = 1, e = 4, f = 5, b = 2, c = 3 }
*/
val remoteWithoutInvalidProviders = remote
.mapValues {
it.value
.filterUnsupportedProviders()
.filterInvalidProviders()
}
.filterValues { it.isNotEmpty() }
val result = local + remoteWithoutInvalidProviders
if (result != remote) {
val missingBlockchains = result.keys - remote.keys
val blockchainsWithoutProviders = remote.filterValues { it.isEmpty() }.keys
recordException(missingBlockchains = missingBlockchains + blockchainsWithoutProviders)
}
return result.guaranteeUrlsEndWithSlash()
}
private fun List<ProviderModel>.filterUnsupportedProviders() = filter {
val isSupportedType = it !is ProviderModel.UnsupportedType
val isSupportedPrivateType = if (it is ProviderModel.Private) {
createPrivateProviderType(it.name) != null
} else {
true
}
isSupportedType && isSupportedPrivateType
}
private fun List<ProviderModel>.filterInvalidProviders() = mapNotNull { provider ->
if (provider is ProviderModel.Public) {
if (isValidUrl(provider.url)) provider else null
} else {
provider
}
}
private fun isValidUrl(url: String): Boolean {
val forbiddenScheme = forbiddenSchemes.firstOrNull { url.startsWith(prefix = it) }
val inputUrl = if (forbiddenScheme != null) url.substringAfter(forbiddenScheme) else url
return PatternsCompat.WEB_URL.matcher(inputUrl).matches()
}
private fun recordException(missingBlockchains: Set<String>) {
val exception = IllegalStateException(
"Remote config does not contain required blockchains or providers information: " +
missingBlockchains.joinToString(),
)
Timber.e(exception)
firebaseCrashlytics.recordException(exception)
}
/*
* Example:
* https://qwe.com --> https://qwe.com/
*/
private fun BlockchainProvidersResponse.guaranteeUrlsEndWithSlash(): BlockchainProvidersResponse {
return mapValues {
it.value.map { provider -> provider.addSlashIfAbsent() }
}
}
private fun ProviderModel.addSlashIfAbsent(): ProviderModel {
return if (this is ProviderModel.Public && url.last() != '/') {
copy(url = "$url/")
} else {
this
}
}
private companion object {
val forbiddenSchemes = listOf("wss://")
}
private suspend fun loadRemote() = runCatching(
dispatcher = dispatchers.io,
block = tangemTechApi::getBlockchainProviders,
)
}

View file

@ -0,0 +1,106 @@
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
/**
* Merger of [BlockchainProvidersResponse]
*
[REDACTED_AUTHOR]
*/
internal object BlockchainProvidersResponseMerger {
private val firebaseCrashlytics by lazy(FirebaseCrashlytics::getInstance)
private val forbiddenSchemes = listOf("wss://")
/**
* Merge blockchains with non-empty providers from [remote] with blockchains from [local]
*
* Example:
* val remote = mapOf("a" to 1, "b" to 2, "c" to 3)
* val local = mapOf("a" to 11, "e" to 4, "f" to 5)
*
* local + remote // { a = 1, e = 4, f = 5, b = 2, c = 3 }
*/
fun merge(local: BlockchainProvidersResponse, remote: BlockchainProvidersResponse): BlockchainProvidersResponse {
val remoteWithoutInvalidProviders = remote
.mapValues {
it.value
.filterUnsupportedProviders()
.filterInvalidProviders()
}
.filterValues { it.isNotEmpty() }
val result = local + remoteWithoutInvalidProviders
if (result != remote) {
val missingBlockchains = result.keys - remote.keys
val blockchainsWithoutProviders = remote.filterValues { it.isEmpty() }.keys
recordException(missingBlockchains = missingBlockchains + blockchainsWithoutProviders)
}
return result.guaranteeUrlsEndWithSlash()
}
private fun List<ProviderModel>.filterUnsupportedProviders() = filter {
val isSupportedType = it !is ProviderModel.UnsupportedType
val isSupportedPrivateType = if (it is ProviderModel.Private) {
createPrivateProviderType(it.name) != null
} else {
true
}
isSupportedType && isSupportedPrivateType
}
private fun List<ProviderModel>.filterInvalidProviders() = mapNotNull { provider ->
if (provider is ProviderModel.Public) {
if (isValidUrl(provider.url)) provider else null
} else {
provider
}
}
private fun isValidUrl(url: String): Boolean {
val forbiddenScheme = forbiddenSchemes.firstOrNull { url.startsWith(prefix = it) }
val inputUrl = if (forbiddenScheme != null) url.substringAfter(forbiddenScheme) else url
return PatternsCompat.WEB_URL.matcher(inputUrl).matches()
}
private fun recordException(missingBlockchains: Set<String>) {
val exception = IllegalStateException(
"Remote config does not contain required blockchains or providers information: " +
missingBlockchains.joinToString(),
)
Timber.e(exception)
firebaseCrashlytics.recordException(exception)
}
/*
* Example:
* https://qwe.com --> https://qwe.com/
*/
private fun BlockchainProvidersResponse.guaranteeUrlsEndWithSlash(): BlockchainProvidersResponse {
return mapValues {
it.value.map { provider -> provider.addSlashIfAbsent() }
}
}
private fun ProviderModel.addSlashIfAbsent(): ProviderModel {
return if (this is ProviderModel.Public && url.last() != '/') {
copy(url = "$url/")
} else {
this
}
}
}

View file

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

View file

@ -0,0 +1,39 @@
package com.tangem.blockchainsdk.providers
import com.tangem.blockchainsdk.converters.BlockchainProviderTypesConverter
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
/**
* Implementation of [BlockchainProvidersTypesManager] in PROD environment
*
* @property blockchainProvidersResponseLoader blockchain providers response loader
* @property blockchainProviderTypesStore blockchain provider types store
*
[REDACTED_AUTHOR]
*/
@Singleton
internal class ProdBlockchainProvidersTypesManager @Inject constructor(
private val blockchainProvidersResponseLoader: BlockchainProvidersResponseLoader,
private val blockchainProviderTypesStore: BlockchainProviderTypesStore,
) : BlockchainProvidersTypesManager {
override fun get(): StateFlow<BlockchainProviderTypes> = blockchainProviderTypesStore.get()
override suspend fun update() {
val response = blockchainProvidersResponseLoader.load()
if (response == null) {
Timber.e("Error loading BlockchainProviderTypes")
return
}
Timber.i("Update BlockchainProviderTypes")
blockchainProviderTypesStore.store(
value = BlockchainProviderTypesConverter.convert(response),
)
}
}