Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-11 18:55:37 +03:00
parent 330ae73357
commit e7efe7b76a
4636 changed files with 234864 additions and 63507 deletions

View file

@ -1,37 +1,9 @@
plugins {
id("com.android.library")
kotlin("android")
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
defaultConfig {
compileSdk = AppConfig.compileSdkVersion
minSdk = AppConfig.minSdkVersion
targetSdk = AppConfig.targetSdkVersion
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
isCoreLibraryDesugaringEnabled = false
}
buildTypes {
create("debug_beta") {
initWith(getByName("release"))
BuildConfigFieldFactory(
fields = listOf(
Field.Environment("release"),
Field.TestActionEnabled(true),
Field.LogEnabled(true),
),
builder = ::buildConfigField,
).create()
}
}
namespace = "com.tangem.lib.auth"
}

View file

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.lib.auth" />

View file

@ -1,14 +0,0 @@
package com.tangem.lib.auth
/**
* Provides auth for tangemTech API
*/
interface AuthProvider {
/**
* Returns authToken for tangem tech api
*/
fun getCardPublicKey(): String
fun getCardId(): String
}

View file

@ -0,0 +1,10 @@
package com.tangem.lib.auth
interface ExpressAuthProvider {
fun getUserId(): String
fun getSessionId(): String
fun getRefCode(): String
}

View file

@ -0,0 +1,6 @@
package com.tangem.lib.auth
interface StakeKitAuthProvider {
fun getApiKey(): String
}

1
libs/blockchain-sdk/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,56 @@
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.libs.blockchain_sdk"
}
dependencies {
// region Core modules
implementation(projects.core.datasource)
implementation(projects.core.configToggles)
implementation(projects.core.utils)
// endregion
// region AndroidX libraries
implementation(deps.androidx.datastore)
// endregion
// region DI libraries
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
// endregion
// region Other libraries
implementation(deps.kotlin.coroutines)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
// endregion
// region Firebase libraries
implementation(platform(deps.firebase.bom))
implementation(deps.firebase.analytics)
implementation(deps.firebase.crashlytics)
// endregion
// region Tangem libraries
implementation(tangemDeps.blockchain) { exclude(module = "joda-time") }
implementation(tangemDeps.card.core)
// endregion
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
}

View file

@ -0,0 +1,17 @@
package com.tangem.blockchainsdk
import com.tangem.blockchain.common.WalletManagerFactory
/**
* Blockchain SDK components factory
*
[REDACTED_AUTHOR]
*/
interface BlockchainSDKFactory {
/** Initialize components */
suspend fun init()
/** Get [WalletManagerFactory] synchronously */
suspend fun getWalletManagerFactorySync(): WalletManagerFactory?
}

View file

@ -0,0 +1,54 @@
package com.tangem.blockchainsdk
import com.tangem.blockchain.common.WalletManagerFactory
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
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
internal typealias BlockchainProvidersResponse = Map<String, List<ProviderModel>>
/**
* Implementation of Blockchain SDK components factory
*
* @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 blockchainProvidersTypesManager: BlockchainProvidersTypesManager,
private val environmentConfigStorage: EnvironmentConfigStorage,
private val walletManagerFactoryCreator: WalletManagerFactoryCreator,
dispatchers: CoroutineDispatcherProvider,
) : BlockchainSDKFactory {
private val mainScope = CoroutineScope(dispatchers.main)
private val walletManagerFactory: Flow<WalletManagerFactory?> = createWalletManagerFactory()
override suspend fun init() {
coroutineScope {
launch { blockchainProvidersTypesManager.update() }
}
}
override suspend fun getWalletManagerFactorySync(): WalletManagerFactory? = walletManagerFactory.firstOrNull()
private fun createWalletManagerFactory(): Flow<WalletManagerFactory?> {
return combine(
flow = environmentConfigStorage.getConfig().map { it.blockchainSdkConfig },
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)
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.blockchainsdk
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.BlockchainFeatureToggles
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.blockchainsdk.featuretoggles.BlockchainSDKFeatureToggles
import com.tangem.blockchainsdk.providers.BlockchainProviderTypes
import timber.log.Timber
import javax.inject.Inject
/**
* Creator of [WalletManagerFactory]
*
* @property accountCreator account creator
* @property blockchainDataStorage blockchain data storage
* @property blockchainSDKLogger blockchain SDK logger
*
[REDACTED_AUTHOR]
*/
internal class WalletManagerFactoryCreator @Inject constructor(
private val accountCreator: AccountCreator,
private val blockchainDataStorage: BlockchainDataStorage,
private val blockchainSDKLogger: BlockchainSDKLogger,
private val blockchainSDKFeatureToggles: BlockchainSDKFeatureToggles,
) {
fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory {
Timber.i("Create WalletManagerFactory")
return WalletManagerFactory(
config = config,
blockchainProviderTypes = blockchainProviderTypes,
accountCreator = accountCreator,
featureToggles = BlockchainFeatureToggles(
isEthereumEIP1559Enabled = blockchainSDKFeatureToggles.isEthereumEIP1559Enabled,
),
blockchainDataStorage = blockchainDataStorage,
loggers = listOf(blockchainSDKLogger),
)
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.blockchainsdk.accountcreator
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.extensions.Result
import com.tangem.common.extensions.toHexString
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CreateUserNetworkAccountBody
internal class DefaultAccountCreator(
private val tangemTechApi: TangemTechApi,
) : AccountCreator {
override suspend fun createAccount(blockchain: Blockchain, walletPublicKey: ByteArray): Result<String> {
val request = CreateUserNetworkAccountBody(
networkId = blockchain.id.removeSuffix("/test"),
walletPublicKey = walletPublicKey.toHexString(),
)
return try {
val response = tangemTechApi.createUserNetworkAccount(
body = request,
).getOrThrow()
Result.Success(response.data.accountId)
} catch (e: Exception) {
Result.Failure(BlockchainSdkError.FailedToCreateAccount)
}
}
}

View file

@ -0,0 +1,59 @@
package com.tangem.blockchainsdk.compatibility
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.datasource.api.markets.models.response.TokenMarketInfoResponse
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
val l2BlockchainsList = Blockchain.entries.filter { it.isL2EthereumNetwork() }
val l2BlockchainsCoinIds = l2BlockchainsList.map { it.toCoinId() }
val ETHEREUM_COIN_ID = Blockchain.Ethereum.toCoinId()
fun getL2CompatibilityTokenComparison(token: UserTokensResponse.Token, currencyId: String): Boolean {
return if (currencyId == ETHEREUM_COIN_ID) {
l2BlockchainsCoinIds.contains(token.id) || currencyId == token.id
} else {
token.id == currencyId
}
}
fun List<CoinsResponse.Coin.Network>.applyL2Compatibility(coinId: String): List<CoinsResponse.Coin.Network> {
return if (coinId == ETHEREUM_COIN_ID) {
val l2Networks = l2BlockchainsList.map {
CoinsResponse.Coin.Network(
networkId = it.toNetworkId(),
)
}
this + l2Networks
} else {
this
}
}
fun TokenMarketInfoResponse.applyL2Compatibility(coinId: String): TokenMarketInfoResponse {
val networks = this.networks ?: return this
return if (coinId == ETHEREUM_COIN_ID) {
val l2Networks = l2BlockchainsList.map {
TokenMarketInfoResponse.Network(
networkId = it.toNetworkId(),
contractAddress = null,
decimalCount = null,
)
}
this.copy(networks = networks + l2Networks)
} else {
this
}
}
fun getTokenIdIfL2Network(tokenId: String): String {
return if (l2BlockchainsCoinIds.contains(tokenId)) {
ETHEREUM_COIN_ID
} else {
tokenId
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.blockchainsdk.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.BlockchainProvidersResponse
import com.tangem.blockchainsdk.providers.BlockchainProviderTypes
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.utils.converter.TwoWayConverter
import timber.log.Timber
/**
* Converts [BlockchainProvidersResponse] to [BlockchainProviderTypes] and vice versa
*
[REDACTED_AUTHOR]
*/
internal object BlockchainProviderTypesConverter :
TwoWayConverter<BlockchainProvidersResponse, BlockchainProviderTypes> {
override fun convert(value: BlockchainProvidersResponse): BlockchainProviderTypes {
return value.mapNotNull { (networkId, blockchainProviders) ->
val blockchain = Blockchain.fromNetworkId(networkId) ?: return@mapNotNull null
val providerTypes = ProviderTypeConverter.convertList(input = blockchainProviders)
providerTypes.forEach {
if (it == null) Timber.e("$blockchain provider type is not supported")
}
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

@ -0,0 +1,35 @@
package com.tangem.blockchainsdk.datastorage
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
/**
* [BlockchainDataStorage] implementation
*
* @property appPreferencesStore app preferences store
*
[REDACTED_AUTHOR]
*/
internal class DefaultBlockchainDataStorage(
private val appPreferencesStore: AppPreferencesStore,
) : BlockchainDataStorage {
override suspend fun getOrNull(key: String): String? {
return appPreferencesStore.getSyncOrNull(key = stringPreferencesKey(name = key))
}
override suspend fun store(key: String, value: String) {
appPreferencesStore.edit {
it[stringPreferencesKey(key)] = value
}
}
override suspend fun remove(key: String) {
appPreferencesStore.edit {
it.remove(stringPreferencesKey(key))
}
}
}

View file

@ -0,0 +1,104 @@
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
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
@InstallIn(SingletonComponent::class)
internal object BlockchainSDKFactoryModule {
@Provides
@Singleton
fun provideBlockchainSDKFactory(
blockchainProvidersTypesManager: BlockchainProvidersTypesManager,
environmentConfigStorage: EnvironmentConfigStorage,
walletManagerFactoryCreator: WalletManagerFactoryCreator,
dispatchers: CoroutineDispatcherProvider,
): BlockchainSDKFactory {
return DefaultBlockchainSDKFactory(
blockchainProvidersTypesManager = blockchainProvidersTypesManager,
environmentConfigStorage = environmentConfigStorage,
walletManagerFactoryCreator = walletManagerFactoryCreator,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideBlockchainProvidersTypesManager(
prodBlockchainProvidersTypesManager: ProdBlockchainProvidersTypesManager,
blockchainProviderTypesStore: BlockchainProviderTypesStore,
changedBlockchainProvidersStore: DataStore<BlockchainProvidersResponse>,
): BlockchainProvidersTypesManager {
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(
tangemTechApi: TangemTechApi,
appPreferencesStore: AppPreferencesStore,
blockchainSDKLogger: BlockchainSDKLogger,
featureTogglesManager: FeatureTogglesManager,
): WalletManagerFactoryCreator {
return WalletManagerFactoryCreator(
accountCreator = DefaultAccountCreator(tangemTechApi),
blockchainDataStorage = DefaultBlockchainDataStorage(appPreferencesStore),
blockchainSDKLogger = blockchainSDKLogger,
blockchainSDKFeatureToggles = DefaultBlockchainSDKFeatureToggles(featureTogglesManager),
)
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.blockchainsdk.di
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object ExcludedBlockchainsModule {
@Provides
@Singleton
fun bindExcludedBlockchains(excludedBlockchainsManager: ExcludedBlockchainsManager): ExcludedBlockchains {
return ExcludedBlockchains(excludedBlockchainsManager)
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.blockchainsdk.featuretoggles
internal interface BlockchainSDKFeatureToggles {
val isEthereumEIP1559Enabled: Boolean
}

View file

@ -0,0 +1,11 @@
package com.tangem.blockchainsdk.featuretoggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
internal class DefaultBlockchainSDKFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : BlockchainSDKFeatureToggles {
override val isEthereumEIP1559Enabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "IS_ETHEREUM_EIP_1559_ENABLED")
}

View file

@ -0,0 +1,18 @@
package com.tangem.blockchainsdk.providers
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.network.providers.ProviderType
import com.tangem.datasource.local.datastore.RuntimeStateStore
import javax.inject.Inject
import javax.inject.Singleton
internal typealias BlockchainProviderTypes = Map<Blockchain, List<ProviderType>>
/**
* Blockchain provider types store
*
[REDACTED_AUTHOR]
*/
@Singleton
class BlockchainProviderTypesStore @Inject constructor() :
RuntimeStateStore<BlockchainProviderTypes> by RuntimeStateStore(emptyMap())

View file

@ -0,0 +1,52 @@
package com.tangem.blockchainsdk.providers
import com.tangem.blockchainsdk.BlockchainProvidersResponse
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
/**
* Loader of [BlockchainProvidersResponse]
*
* @property tangemTechApi tangem tech api
* @property blockchainProvidersStorage blockchain providers storage
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
*/
@Singleton
internal class BlockchainProvidersResponseLoader @Inject constructor(
private val tangemTechApi: TangemTechApi,
private val blockchainProvidersStorage: BlockchainProvidersStorage,
private val dispatchers: CoroutineDispatcherProvider,
) {
/** Load [BlockchainProvidersResponse] */
suspend fun load(): BlockchainProvidersResponse? {
val localResponse = loadLocal().ifEmpty { return null }
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() = runCatching(
dispatcher = dispatchers.io,
block = tangemTechApi::getBlockchainProviders,
)
}

View file

@ -0,0 +1,105 @@
package com.tangem.blockchainsdk.providers
import androidx.core.util.PatternsCompat
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchainsdk.BlockchainProvidersResponse
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 { model ->
val isSupportedType = model !is ProviderModel.UnsupportedType
val isSupportedPrivateType = if (model is ProviderModel.Private) {
ProviderTypeIdMapping.entries.any { it.id == model.name }
} 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.Flow
/** Blockchain providers types manager */
interface BlockchainProvidersTypesManager {
fun get(): Flow<BlockchainProviderTypes>
suspend fun update()
}

View file

@ -0,0 +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 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]
*/
internal class DevBlockchainProvidersTypesManager(
private val prodBlockchainProvidersTypesManager: ProdBlockchainProvidersTypesManager,
private val blockchainProviderTypesStore: BlockchainProviderTypesStore,
private val changedBlockchainProvidersStore: DataStore<BlockchainProvidersResponse>,
) : MutableBlockchainProvidersTypesManager {
override fun get(): Flow<BlockchainProviderTypes> = changedBlockchainProvidersStore.data
.map(BlockchainProviderTypesConverter::convert)
override suspend fun update() {
prodBlockchainProvidersTypesManager.update()
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>) {
changedBlockchainProvidersStore.updateData {
val providerModels = BlockchainProviderTypesConverter.convertBack(value = mapOf(blockchain to providers))
it + providerModels
}
}
override suspend fun isMatchWithMerged(): Boolean {
val initial = blockchainProviderTypesStore.get().value
val changed = changedBlockchainProvidersStore.data.firstOrNull().orEmpty()
return initial == BlockchainProviderTypesConverter.convert(changed)
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.blockchainsdk.providers
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.network.providers.ProviderType
/**
* Mutable blockchain providers types manager
*
[REDACTED_AUTHOR]
*/
interface MutableBlockchainProvidersTypesManager : BlockchainProvidersTypesManager {
/** 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

@ -0,0 +1,39 @@
package com.tangem.blockchainsdk.providers
import com.tangem.blockchainsdk.converters.BlockchainProviderTypesConverter
import kotlinx.coroutines.flow.Flow
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(): Flow<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),
)
}
}

View file

@ -0,0 +1,28 @@
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),
AlephiumTangem(id = "tangemAlephium", providerType = ProviderType.Alephium.Tangem),
;
}

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

@ -0,0 +1,464 @@
package com.tangem.blockchainsdk.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.ReserveAmountProvider
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import java.math.BigDecimal
@Suppress("ComplexMethod", "LongMethod")
fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
return when (networkId) {
"alephium" -> Blockchain.Alephium
"alephium/test" -> Blockchain.AlephiumTestnet
"arbitrum-one" -> Blockchain.Arbitrum
"arbitrum-one/test" -> Blockchain.ArbitrumTestnet
"avalanche", "avalanche-2" -> Blockchain.Avalanche
"avalanche/test", "avalanche-2/test" -> Blockchain.AvalancheTestnet
"binancecoin" -> Blockchain.Binance
"binancecoin/test" -> Blockchain.BinanceTestnet
"binance-smart-chain" -> Blockchain.BSC
"binance-smart-chain/test" -> Blockchain.BSCTestnet
"ethereum" -> Blockchain.Ethereum
"ethereum/test" -> Blockchain.EthereumTestnet
"ethereum-classic" -> Blockchain.EthereumClassic
"ethereum-classic/test" -> Blockchain.EthereumClassicTestnet
"polygon-pos", "matic-network" -> Blockchain.Polygon
"polygon-pos/test", "matic-network/test" -> Blockchain.PolygonTestnet
"solana" -> Blockchain.Solana
"solana/test" -> Blockchain.SolanaTestnet
"fantom" -> Blockchain.Fantom
"fantom/test" -> Blockchain.FantomTestnet
"bitcoin" -> Blockchain.Bitcoin
"bitcoin/test" -> Blockchain.BitcoinTestnet
"bitcoin-cash" -> Blockchain.BitcoinCash
"bitcoin-cash/test" -> Blockchain.BitcoinCashTestnet
"cardano" -> Blockchain.Cardano
"dogecoin" -> Blockchain.Dogecoin
"ducatus" -> Blockchain.Ducatus
"litecoin" -> Blockchain.Litecoin
"rootstock" -> Blockchain.RSK
"stellar" -> Blockchain.Stellar
"stellar/test" -> Blockchain.StellarTestnet
"tezos" -> Blockchain.Tezos
"tron" -> Blockchain.Tron
"tron/test" -> Blockchain.TronTestnet
"xrp", "ripple" -> Blockchain.XRP
"xdai" -> Blockchain.Gnosis
"ethereum-pow-iou" -> Blockchain.EthereumPow
"ethereum-pow-iou/test" -> Blockchain.EthereumPowTestnet
"ethereumfair", "dischain" -> Blockchain.Dischain // for old client compatibility
"polkadot" -> Blockchain.Polkadot
"polkadot/test" -> Blockchain.PolkadotTestnet
"kusama" -> Blockchain.Kusama
"optimistic-ethereum" -> Blockchain.Optimism
"optimistic-ethereum/test" -> Blockchain.OptimismTestnet
"dash" -> Blockchain.Dash
"kaspa" -> Blockchain.Kaspa
"the-open-network" -> Blockchain.TON
"the-open-network/test" -> Blockchain.TONTestnet
"kava" -> Blockchain.Kava
"kava/test" -> Blockchain.KavaTestnet
"ravencoin" -> Blockchain.Ravencoin
"ravencoin/test" -> Blockchain.RavencoinTestnet
"cosmos" -> Blockchain.Cosmos
"cosmos/test" -> Blockchain.CosmosTestnet
"terra" -> Blockchain.TerraV1
"terra-2" -> Blockchain.TerraV2
"cronos" -> Blockchain.Cronos
"telos" -> Blockchain.Telos
"telos/test" -> Blockchain.TelosTestnet
"aleph-zero" -> Blockchain.AlephZero
"aleph-zero/test" -> Blockchain.AlephZeroTestnet
"octaspace" -> Blockchain.OctaSpace
"octaspace/test" -> Blockchain.OctaSpaceTestnet
"chia" -> Blockchain.Chia
"chia/test" -> Blockchain.ChiaTestnet
"near-protocol" -> Blockchain.Near
"near-protocol/test" -> Blockchain.NearTestnet
"decimal" -> Blockchain.Decimal
"decimal/test" -> Blockchain.DecimalTestnet
"xdc-network" -> Blockchain.XDC
"xdc-network/test" -> Blockchain.XDCTestnet
"vechain" -> Blockchain.VeChain
"vechain/test" -> Blockchain.VeChainTestnet
"aptos" -> Blockchain.Aptos
"aptos/test" -> Blockchain.AptosTestnet
"playa3ull-games" -> Blockchain.Playa3ull
"shibarium" -> Blockchain.Shibarium
"shibarium/test" -> Blockchain.ShibariumTestnet
"algorand" -> Blockchain.Algorand
"algorand/test" -> Blockchain.AlgorandTestnet
"hedera-hashgraph" -> Blockchain.Hedera
"hedera-hashgraph/test" -> Blockchain.HederaTestnet
"aurora" -> Blockchain.Aurora
"aurora/test" -> Blockchain.AuroraTestnet
"areon-network" -> Blockchain.Areon
"areon-network/test" -> Blockchain.AreonTestnet
"pulsechain" -> Blockchain.PulseChain
"pulsechain/test" -> Blockchain.PulseChainTestnet
"zksync" -> Blockchain.ZkSyncEra
"zksync/test" -> Blockchain.ZkSyncEraTestnet
"moonbeam" -> Blockchain.Moonbeam
"moonbeam/test" -> Blockchain.MoonbeamTestnet
"manta-pacific" -> Blockchain.Manta
"manta-pacific/test" -> Blockchain.MantaTestnet
"polygon-zkevm" -> Blockchain.PolygonZkEVM
"polygon-zkevm/test" -> Blockchain.PolygonZkEVMTestnet
"nexa" -> Blockchain.Nexa // FIXME
"nexa/test" -> Blockchain.NexaTestnet // FIXME
"radiant" -> Blockchain.Radiant
"fact0rn" -> Blockchain.Fact0rn
"moonriver" -> Blockchain.Moonriver
"moonriver/test" -> Blockchain.MoonriverTestnet
"mantle" -> Blockchain.Mantle
"mantle/test" -> Blockchain.MantleTestnet
"flare-network" -> Blockchain.Flare
"flare-network/test" -> Blockchain.FlareTestnet
"taraxa" -> Blockchain.Taraxa
"taraxa/test" -> Blockchain.TaraxaTestnet
"base" -> Blockchain.Base
"base/test" -> Blockchain.BaseTestnet
"koinos" -> Blockchain.Koinos
"koinos/test" -> Blockchain.KoinosTestnet
"joystream" -> Blockchain.Joystream
"bittensor" -> Blockchain.Bittensor
"filecoin" -> Blockchain.Filecoin
"blast" -> Blockchain.Blast
"blast/test" -> Blockchain.BlastTestnet
"cyber" -> Blockchain.Cyber
"cyber/test" -> Blockchain.CyberTestnet
"sei-network" -> Blockchain.Sei
"sei-network/test" -> Blockchain.SeiTestnet
"internet-computer" -> Blockchain.InternetComputer
"sui" -> Blockchain.Sui
"sui/test" -> Blockchain.SuiTestnet
"energy-web-chain" -> Blockchain.EnergyWebChain
"energy-web-chain/test" -> Blockchain.EnergyWebChainTestnet
"energy-web-x" -> Blockchain.EnergyWebX
"energy-web-x/test" -> Blockchain.EnergyWebXTestnet
"add_later" -> Blockchain.Casper
"add_later_test" -> Blockchain.CasperTestnet
"core" -> Blockchain.Core
"core/test" -> Blockchain.CoreTestnet
"casper-network" -> Blockchain.Casper
"casper-network/test" -> Blockchain.CasperTestnet
"chiliz" -> Blockchain.Chiliz
"chiliz/test" -> Blockchain.ChilizTestnet
"vanar-chain" -> Blockchain.VanarChain
"vanar-chain/test" -> Blockchain.VanarChainTestnet
"xodex" -> Blockchain.Xodex
"canxium" -> Blockchain.Canxium
"clore-ai" -> Blockchain.Clore
"dione" -> Blockchain.OdysseyChain
"dione/test" -> Blockchain.OdysseyChainTestnet
"bitrock" -> Blockchain.Bitrock
"bitrock/test" -> Blockchain.BitrockTestnet
"sonic" -> Blockchain.Sonic
"sonic/test" -> Blockchain.SonicTestnet
"apechain" -> Blockchain.ApeChain
"apechain/test" -> Blockchain.ApeChainTestnet
"kaspa/test" -> Blockchain.KaspaTestnet
else -> null
}
}
@Suppress("ComplexMethod", "LongMethod")
fun Blockchain.toNetworkId(): String {
return when (this) {
Blockchain.Unknown -> "unknown"
Blockchain.Alephium -> "alephium"
Blockchain.AlephiumTestnet -> "alephium/test"
Blockchain.Arbitrum -> "arbitrum-one"
Blockchain.ArbitrumTestnet -> "arbitrum-one/test"
Blockchain.Avalanche -> "avalanche"
Blockchain.AvalancheTestnet -> "avalanche/test"
Blockchain.Binance -> "binancecoin"
Blockchain.BinanceTestnet -> "binancecoin/test"
Blockchain.BSC -> "binance-smart-chain"
Blockchain.BSCTestnet -> "binance-smart-chain/test"
Blockchain.Bitcoin -> "bitcoin"
Blockchain.BitcoinTestnet -> "bitcoin/test"
Blockchain.BitcoinCash -> "bitcoin-cash"
Blockchain.BitcoinCashTestnet -> "bitcoin-cash/test"
Blockchain.Cardano -> "cardano"
Blockchain.Dogecoin -> "dogecoin"
Blockchain.Ducatus -> "ducatus"
Blockchain.Ethereum -> "ethereum"
Blockchain.EthereumTestnet -> "ethereum/test"
Blockchain.EthereumClassic -> "ethereum-classic"
Blockchain.EthereumClassicTestnet -> "ethereum-classic/test"
Blockchain.Fantom -> "fantom"
Blockchain.FantomTestnet -> "fantom/test"
Blockchain.Litecoin -> "litecoin"
Blockchain.Polygon -> "polygon-pos"
Blockchain.PolygonTestnet -> "polygon-pos/test"
Blockchain.RSK -> "rootstock"
Blockchain.Stellar -> "stellar"
Blockchain.StellarTestnet -> "stellar/test"
Blockchain.Solana -> "solana"
Blockchain.SolanaTestnet -> "solana/test"
Blockchain.Tezos -> "tezos"
Blockchain.XRP -> "xrp"
Blockchain.Tron -> "tron"
Blockchain.TronTestnet -> "tron/test"
Blockchain.Gnosis -> "xdai"
Blockchain.EthereumPow -> "ethereum-pow-iou"
Blockchain.EthereumPowTestnet -> "ethereum-pow-iou/test"
Blockchain.Dischain -> "ethereumfair" // for backend compatibility
Blockchain.Polkadot -> "polkadot"
Blockchain.PolkadotTestnet -> "polkadot/test"
Blockchain.Kusama -> "kusama"
Blockchain.Optimism -> "optimistic-ethereum"
Blockchain.OptimismTestnet -> "optimistic-ethereum/test"
Blockchain.Dash -> "dash"
Blockchain.Kaspa -> "kaspa"
Blockchain.KaspaTestnet -> "kaspa/test"
Blockchain.TON -> "the-open-network"
Blockchain.TONTestnet -> "the-open-network/test"
Blockchain.Kava -> "kava"
Blockchain.KavaTestnet -> "kava/test"
Blockchain.Ravencoin -> "ravencoin"
Blockchain.RavencoinTestnet -> "ravencoin/test"
Blockchain.Cosmos -> "cosmos"
Blockchain.CosmosTestnet -> "cosmos/test"
Blockchain.TerraV1 -> "terra"
Blockchain.TerraV2 -> "terra-2"
Blockchain.Cronos -> "cronos"
Blockchain.Telos -> "telos"
Blockchain.TelosTestnet -> "telos/test"
Blockchain.AlephZero -> "aleph-zero"
Blockchain.AlephZeroTestnet -> "aleph-zero/test"
Blockchain.OctaSpace -> "octaspace"
Blockchain.OctaSpaceTestnet -> "octaspace/test"
Blockchain.Chia -> "chia"
Blockchain.ChiaTestnet -> "chia/test"
Blockchain.Near -> "near-protocol"
Blockchain.NearTestnet -> "near-protocol/test"
Blockchain.Decimal -> "decimal"
Blockchain.DecimalTestnet -> "decimal/test"
Blockchain.XDC -> "xdc-network"
Blockchain.XDCTestnet -> "xdc-network/test"
Blockchain.VeChain -> "vechain"
Blockchain.VeChainTestnet -> "vechain/test"
Blockchain.Aptos -> "aptos"
Blockchain.AptosTestnet -> "aptos/test"
Blockchain.Playa3ull -> "playa3ull-games"
Blockchain.Shibarium -> "shibarium"
Blockchain.ShibariumTestnet -> "shibarium/test"
Blockchain.Algorand -> "algorand"
Blockchain.AlgorandTestnet -> "algorand/test"
Blockchain.Hedera -> "hedera-hashgraph"
Blockchain.HederaTestnet -> "hedera-hashgraph/test"
Blockchain.Aurora -> "aurora"
Blockchain.AuroraTestnet -> "aurora/test"
Blockchain.Areon -> "areon-network"
Blockchain.AreonTestnet -> "areon-network/test"
Blockchain.PulseChain -> "pulsechain"
Blockchain.PulseChainTestnet -> "pulsechain/test"
Blockchain.ZkSyncEra -> "zksync"
Blockchain.ZkSyncEraTestnet -> "zksync/test"
Blockchain.Moonbeam -> "moonbeam"
Blockchain.MoonbeamTestnet -> "moonbeam/test"
Blockchain.Manta -> "manta-pacific"
Blockchain.MantaTestnet -> "manta-pacific/test"
Blockchain.PolygonZkEVM -> "polygon-zkevm"
Blockchain.PolygonZkEVMTestnet -> "polygon-zkevm/test"
Blockchain.Nexa -> "nexa" // FIXME
Blockchain.NexaTestnet -> "nexa/test" // FIXME
Blockchain.Radiant -> "radiant"
Blockchain.Fact0rn -> "fact0rn"
Blockchain.Moonriver -> "moonriver"
Blockchain.MoonriverTestnet -> "moonriver/test"
Blockchain.Mantle -> "mantle"
Blockchain.MantleTestnet -> "mantle/test"
Blockchain.Flare -> "flare-network"
Blockchain.FlareTestnet -> "flare-network/test"
Blockchain.Taraxa -> "taraxa"
Blockchain.TaraxaTestnet -> "taraxa/test"
Blockchain.Base -> "base"
Blockchain.BaseTestnet -> "base/test"
Blockchain.Koinos -> "koinos"
Blockchain.KoinosTestnet -> "koinos/test"
Blockchain.Joystream -> "joystream"
Blockchain.Bittensor -> "bittensor"
Blockchain.Filecoin -> "filecoin"
Blockchain.Blast -> "blast"
Blockchain.BlastTestnet -> "blast/test"
Blockchain.Cyber -> "cyber"
Blockchain.CyberTestnet -> "cyber/test"
Blockchain.Sei -> "sei-network"
Blockchain.SeiTestnet -> "sei-network/test"
Blockchain.InternetComputer -> "internet-computer"
Blockchain.Sui -> "sui"
Blockchain.SuiTestnet -> "sui/test"
Blockchain.EnergyWebChain -> "energy-web-chain"
Blockchain.EnergyWebChainTestnet -> "energy-web-chain/test"
Blockchain.EnergyWebX -> "energy-web-x"
Blockchain.EnergyWebXTestnet -> "energy-web-x/test"
Blockchain.Casper -> "casper-network"
Blockchain.CasperTestnet -> "casper-network/test"
Blockchain.Core -> "core"
Blockchain.CoreTestnet -> "core/test"
Blockchain.Chiliz -> "chiliz"
Blockchain.ChilizTestnet -> "chiliz/test"
Blockchain.VanarChain -> "vanar-chain"
Blockchain.VanarChainTestnet -> "vanar-chain/test"
Blockchain.Xodex -> "xodex"
Blockchain.Canxium -> "canxium"
Blockchain.Clore -> "clore-ai"
Blockchain.OdysseyChain -> "dione"
Blockchain.OdysseyChainTestnet -> "dione/test"
Blockchain.Bitrock -> "bitrock"
Blockchain.BitrockTestnet -> "bitrock/test"
Blockchain.Sonic -> "sonic"
Blockchain.SonicTestnet -> "sonic/test"
Blockchain.ApeChain -> "apechain"
Blockchain.ApeChainTestnet -> "apechain/test"
}
}
/**
* CoinId is id from tangem backend response coin "id" field
*/
@Suppress("ComplexMethod", "LongMethod")
fun Blockchain.toCoinId(): String {
return when (this) {
Blockchain.Binance, Blockchain.BinanceTestnet, Blockchain.BSC, Blockchain.BSCTestnet -> "binancecoin"
Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> "bitcoin"
Blockchain.BitcoinCash, Blockchain.BitcoinCashTestnet -> "bitcoin-cash"
Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ethereum"
Blockchain.EthereumClassic, Blockchain.EthereumClassicTestnet -> "ethereum-classic"
Blockchain.Stellar, Blockchain.StellarTestnet -> "stellar"
Blockchain.Cardano -> "cardano"
Blockchain.Polygon, Blockchain.PolygonTestnet -> NEW_POLYGON_NAME
Blockchain.Alephium, Blockchain.AlephiumTestnet -> "alephium"
Blockchain.Arbitrum, Blockchain.ArbitrumTestnet -> "arbitrum-one"
Blockchain.Avalanche, Blockchain.AvalancheTestnet -> "avalanche-2"
Blockchain.Solana, Blockchain.SolanaTestnet -> "solana"
Blockchain.Fantom, Blockchain.FantomTestnet -> "fantom"
Blockchain.Tron, Blockchain.TronTestnet -> "tron"
Blockchain.Polkadot, Blockchain.PolkadotTestnet -> "polkadot"
Blockchain.Ducatus -> "ducatus"
Blockchain.Litecoin -> "litecoin"
Blockchain.RSK -> "rootstock"
Blockchain.Tezos -> "tezos"
Blockchain.XRP -> "ripple"
Blockchain.Dogecoin -> "dogecoin"
Blockchain.Gnosis -> "xdai"
Blockchain.EthereumPow, Blockchain.EthereumPowTestnet -> "ethereum-pow-iou"
Blockchain.Dischain -> "ethereumfair" // for backend compatibility
Blockchain.Kusama -> "kusama"
Blockchain.Optimism, Blockchain.OptimismTestnet -> "optimistic-ethereum"
Blockchain.Dash -> "dash"
Blockchain.Kaspa, Blockchain.KaspaTestnet -> "kaspa"
Blockchain.TON, Blockchain.TONTestnet -> "the-open-network"
Blockchain.Kava, Blockchain.KavaTestnet -> "kava"
Blockchain.Ravencoin, Blockchain.RavencoinTestnet -> "ravencoin"
Blockchain.Cosmos, Blockchain.CosmosTestnet -> "cosmos"
Blockchain.TerraV1 -> "terra-luna"
Blockchain.TerraV2 -> "terra-luna-2"
Blockchain.Cronos -> "crypto-com-chain"
Blockchain.Telos, Blockchain.TelosTestnet -> "telos"
Blockchain.AlephZero, Blockchain.AlephZeroTestnet -> "aleph-zero"
Blockchain.OctaSpace, Blockchain.OctaSpaceTestnet -> "octaspace"
Blockchain.Chia, Blockchain.ChiaTestnet -> "chia"
Blockchain.Near -> "near"
Blockchain.NearTestnet -> "near/test"
Blockchain.Decimal, Blockchain.DecimalTestnet -> "decimal"
Blockchain.XDC, Blockchain.XDCTestnet -> "xdce-crowd-sale"
Blockchain.VeChain, Blockchain.VeChainTestnet -> "vechain"
Blockchain.Aptos -> "aptos"
Blockchain.AptosTestnet -> "aptos/test"
Blockchain.Playa3ull -> "playa3ull-games-2"
Blockchain.Shibarium -> "bone-shibaswap"
Blockchain.ShibariumTestnet -> "bone-shibaswap/test"
Blockchain.Algorand -> "algorand"
Blockchain.AlgorandTestnet -> "algorand/test"
Blockchain.Unknown -> "unknown"
Blockchain.Hedera -> "hedera-hashgraph"
Blockchain.HederaTestnet -> "hedera-hashgraph/test"
Blockchain.Aurora, Blockchain.AuroraTestnet -> "aurora-ethereum"
Blockchain.Areon, Blockchain.AreonTestnet -> "areon-network"
Blockchain.PulseChain, Blockchain.PulseChainTestnet -> "pulsechain"
Blockchain.ZkSyncEra, Blockchain.ZkSyncEraTestnet -> "zksync-ethereum"
Blockchain.Moonbeam, Blockchain.MoonbeamTestnet -> "moonbeam"
Blockchain.Manta, Blockchain.MantaTestnet -> "manta-pacific"
Blockchain.PolygonZkEVM, Blockchain.PolygonZkEVMTestnet -> "polygon-zkevm-ethereum"
Blockchain.Nexa, Blockchain.NexaTestnet -> "nexa" // FIXME
Blockchain.Radiant -> "radiant"
Blockchain.Fact0rn -> "fact0rn"
Blockchain.Moonriver, Blockchain.MoonriverTestnet -> "moonriver"
Blockchain.Mantle, Blockchain.MantleTestnet -> "mantle"
Blockchain.Flare, Blockchain.FlareTestnet -> "flare-networks"
Blockchain.Taraxa, Blockchain.TaraxaTestnet -> "taraxa"
Blockchain.Base, Blockchain.BaseTestnet -> "base-ethereum"
Blockchain.Koinos, Blockchain.KoinosTestnet -> "koinos"
Blockchain.Joystream -> "joystream"
Blockchain.Bittensor -> "bittensor"
Blockchain.Filecoin -> "filecoin"
Blockchain.Blast, Blockchain.BlastTestnet -> "blast-ethereum"
Blockchain.Cyber, Blockchain.CyberTestnet -> "cyber-ethereum"
Blockchain.Sei, Blockchain.SeiTestnet -> "sei-network"
Blockchain.InternetComputer -> "internet-computer"
Blockchain.Sui, Blockchain.SuiTestnet -> "sui"
Blockchain.EnergyWebChain, Blockchain.EnergyWebChainTestnet -> "energy-web-token"
Blockchain.EnergyWebX, Blockchain.EnergyWebXTestnet -> "energy-web-token"
Blockchain.Casper, Blockchain.CasperTestnet -> "casper-network"
Blockchain.Core, Blockchain.CoreTestnet -> "coredaoorg"
Blockchain.Chiliz, Blockchain.ChilizTestnet -> "chiliz"
Blockchain.VanarChain, Blockchain.VanarChainTestnet -> "vanar-chain"
Blockchain.Xodex -> "xodex"
Blockchain.Canxium -> "canxium"
Blockchain.Clore -> "clore-ai"
Blockchain.OdysseyChain, Blockchain.OdysseyChainTestnet -> "dione"
Blockchain.Bitrock, Blockchain.BitrockTestnet -> "bitrock"
Blockchain.Sonic, Blockchain.SonicTestnet -> "sonic-3"
Blockchain.ApeChain, Blockchain.ApeChainTestnet -> "apecoin"
}
}
/**
* New CoinId to existing coin "id" field.
* To support both old and new coin id.
*/
fun Blockchain.toMigratedCoinId(): String = when (this) {
Blockchain.Polygon, Blockchain.PolygonTestnet -> NEW_POLYGON_NAME
else -> toCoinId()
}
fun Blockchain.amountToCreateAccount(walletManager: WalletManager, token: Token? = null): BigDecimal? {
return when (this) {
Blockchain.Stellar -> {
val reserve = if (walletManager is ReserveAmountProvider) {
walletManager.getReserveAmount()
} else {
BigDecimal.ONE
}
if (token?.symbol == NODL) BigDecimal(NODL_AMOUNT_TO_CREATE_ACCOUNT) else reserve
}
Blockchain.XRP -> {
if (walletManager is ReserveAmountProvider) {
walletManager.getReserveAmount()
} else {
BigDecimal.ONE
}
}
Blockchain.Near, Blockchain.NearTestnet -> 0.00182.toBigDecimal()
Blockchain.Aptos, Blockchain.AptosTestnet,
Blockchain.Filecoin,
Blockchain.Casper, Blockchain.CasperTestnet,
-> BigDecimal.ZERO
else -> null
}
}
fun Blockchain.minimalAmount(): BigDecimal {
return BigDecimal.ONE.movePointLeft(decimals())
}
const val OLD_POLYGON_NAME = "matic-network"
const val NEW_POLYGON_NAME = "polygon-ecosystem-token"
private const val NODL = "NODL"
private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5

View file

@ -0,0 +1,45 @@
package com.tangem.blockchainsdk.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
import org.jetbrains.annotations.TestOnly
import javax.inject.Inject
class ExcludedBlockchains @Inject internal constructor(
private val excludedBlockchainsManager: ExcludedBlockchainsManager,
) : Set<Blockchain> {
private val excludedBlockchains: Set<Blockchain> by lazy(mode = LazyThreadSafetyMode.NONE) {
excludedBlockchainsManager.excludedBlockchainsIds.fold(mutableSetOf()) { acc, blockchainId ->
val blockchain = Blockchain.fromId(blockchainId)
acc.add(blockchain)
blockchain.getTestnetVersion()?.let { acc.add(it) }
acc
}
}
override val size: Int
get() = excludedBlockchains.size
@TestOnly
constructor() : this(
excludedBlockchainsManager = object : ExcludedBlockchainsManager {
override val excludedBlockchainsIds: Set<String> = emptySet()
override suspend fun init() {
/* no-op */
}
},
)
override fun contains(element: Blockchain): Boolean = excludedBlockchains.contains(element)
override fun containsAll(elements: Collection<Blockchain>): Boolean = excludedBlockchains.containsAll(elements)
override fun isEmpty(): Boolean = excludedBlockchains.isEmpty()
override fun iterator(): Iterator<Blockchain> = excludedBlockchains.iterator()
}

View file

@ -0,0 +1,88 @@
package com.tangem.blockchainsdk.providers
import com.google.common.truth.Truth
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchainsdk.providers.BlockchainProvidersResponseMergerTest.Companion.localResponse
import com.tangem.blockchainsdk.providers.BlockchainProvidersResponseMergerTest.Companion.remoteResponse
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.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
/**
[REDACTED_AUTHOR]
*/
internal class BlockchainProvidersResponseLoaderTest {
private val tangemTechApi = mockk<TangemTechApi>()
private val blockchainProvidersStorage = mockk<BlockchainProvidersStorage>()
private val loader = BlockchainProvidersResponseLoader(
tangemTechApi = tangemTechApi,
blockchainProvidersStorage = blockchainProvidersStorage,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@Before
fun setup() {
mockkStatic(FirebaseCrashlytics::class)
val firebaseCrashlytics = mockk<FirebaseCrashlytics>()
every { FirebaseCrashlytics.getInstance() } returns firebaseCrashlytics
every { firebaseCrashlytics.recordException(any()) } just Runs
}
@Test
fun test_if_local_config_is_empty() = runTest {
coEvery { blockchainProvidersStorage.getConfigSync() } returns emptyMap()
val expected = null
val actual = loader.load()
coVerifyOrder { blockchainProvidersStorage.getConfigSync() }
coVerify(inverse = true) { tangemTechApi.getBlockchainProviders() }
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun test_if_remote_config_loading_is_failed() = runTest {
coEvery { blockchainProvidersStorage.getConfigSync() } returns localResponse
coEvery { tangemTechApi.getBlockchainProviders() } throws IllegalStateException("Test exception")
val expected = localResponse
val actual = loader.load()
coVerifyOrder {
blockchainProvidersStorage.getConfigSync()
tangemTechApi.getBlockchainProviders()
}
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun test_if_remote_config_is_loaded_successfully() = runTest {
val eth = "ethereum" to listOf(ProviderModel.Private(name = "nownodes"))
coEvery { blockchainProvidersStorage.getConfigSync() } returns localResponse + eth
coEvery { tangemTechApi.getBlockchainProviders() } returns remoteResponse
// Because configs are merged in BlockchainProvidersResponseMerger
val expected = remoteResponse + eth
val actual = loader.load()
coVerifyOrder {
blockchainProvidersStorage.getConfigSync()
tangemTechApi.getBlockchainProviders()
}
Truth.assertThat(actual).isEqualTo(expected)
}
}

View file

@ -0,0 +1,205 @@
package com.tangem.blockchainsdk.providers
import com.google.common.truth.Truth
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchainsdk.BlockchainProvidersResponse
import com.tangem.datasource.local.config.providers.models.ProviderModel
import io.mockk.*
import org.junit.Before
import org.junit.Test
/**
[REDACTED_AUTHOR]
*/
internal class BlockchainProvidersResponseMergerTest {
@Before
fun setup() {
mockkStatic(FirebaseCrashlytics::class)
val firebaseCrashlytics = mockk<FirebaseCrashlytics>()
every { FirebaseCrashlytics.getInstance() } returns firebaseCrashlytics
every { firebaseCrashlytics.recordException(any()) } just Runs
}
@Test
fun test_if_both_configs_are_empty() {
val expected = emptyMap<String, List<ProviderModel>>()
val actual = BlockchainProvidersResponseMerger.merge(
local = emptyMap(),
remote = emptyMap(),
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun test_if_local_config_is_empty() {
val expected = remoteResponse
val actual = BlockchainProvidersResponseMerger.merge(
local = emptyMap(),
remote = remoteResponse,
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun test_if_remote_config_is_empty() {
val expected = localResponse
val actual = BlockchainProvidersResponseMerger.merge(
local = localResponse,
remote = emptyMap(),
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun test_if_both_configs_are_not_empty() {
val expected = remoteResponse
val actual = BlockchainProvidersResponseMerger.merge(
local = localResponse,
remote = remoteResponse,
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun test_if_configs_are_equal() {
val expected = remoteResponse
val actual = BlockchainProvidersResponseMerger.merge(
local = remoteResponse,
remote = remoteResponse,
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun test_if_local_config_has_blockchain_with_empty_providers() {
val eth = "ethereum" to emptyList<ProviderModel>()
val localResponseWithEth = localResponse + eth
val expected = localResponseWithEth + remoteResponse
val actual = BlockchainProvidersResponseMerger.merge(
local = localResponseWithEth,
remote = remoteResponse,
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun test_if_remote_config_has_blockchain_with_empty_providers() {
val expected = remoteResponse
val eth = "ethereum" to emptyList<ProviderModel>()
val remoteWithEth = remoteResponse + eth
val actual = BlockchainProvidersResponseMerger.merge(
local = localResponse,
remote = remoteWithEth,
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun test_if_remote_config_doesnt_contain_local_providers() {
val remoteWithoutLocal = remoteResponse - localResponse.keys
/**
* The expected result does not contain local blockchains, since they can be disabled remotely.
* For the opposite case, see [test_if_both_configs_are_not_empty].
*/
val expected = remoteResponse
val actual = BlockchainProvidersResponseMerger.merge(
local = localResponse,
remote = remoteWithoutLocal,
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun test_if_remote_config_contains_unsupported_providers() {
val nowNodesProvider = ProviderModel.Private(name = "nownodes")
val eth = "ethereum" to listOf(ProviderModel.UnsupportedType, nowNodesProvider)
val remoteWithEth = remoteResponse + eth
val expected = remoteResponse + ("ethereum" to listOf(nowNodesProvider))
val actual = BlockchainProvidersResponseMerger.merge(
local = localResponse,
remote = remoteWithEth,
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun test_if_remote_config_contains_invalid_public_providers() {
val eth = "ethereum" to listOf(ProviderModel.UnsupportedType, ProviderModel.Public("adbw2138"))
val remoteWithEth = remoteResponse + eth
val expected = remoteResponse
val actual = BlockchainProvidersResponseMerger.merge(
local = localResponse,
remote = remoteWithEth,
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun test_if_configs_contain_public_providers_without_slash_in_the_end() {
val eth = "ethereum" to listOf(ProviderModel.Public("https://qwe.com"))
val kaspa = "kaspa" to listOf(ProviderModel.Public("https://qwe.com"))
val localWithKaspa = localResponse + kaspa
val remoteWithEth = remoteResponse + eth
val expected = remoteResponse + eth.addSlash() + kaspa.addSlash()
val actual = BlockchainProvidersResponseMerger.merge(
local = localWithKaspa,
remote = remoteWithEth,
)
Truth.assertThat(actual).isEqualTo(expected)
}
companion object {
val localResponse: BlockchainProvidersResponse = mapOf(
"aptos" to listOf(ProviderModel.Private(name = "nownodes")),
"algorand" to listOf(
ProviderModel.Private(name = "nownodes"),
ProviderModel.Public(url = "https://public_alg.com/"),
),
)
// local + bitcoin
val remoteResponse: BlockchainProvidersResponse = mapOf(
"aptos" to listOf(ProviderModel.Private(name = "nownodes")),
"algorand" to listOf(
ProviderModel.Private(name = "nownodes"),
ProviderModel.Public(url = "https://public_alg.com/"),
),
"bitcoin" to listOf(ProviderModel.Private(name = "blockchair")),
)
private fun Pair<String, List<ProviderModel.Public>>.addSlash(): Pair<String, List<ProviderModel.Public>> {
return first to second.map { it.copy(url = "${it.url}/") }
}
}
}

View file

@ -1,15 +1,26 @@
plugins {
id("java-library")
id("org.jetbrains.kotlin.jvm")
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.kotlin.serialization)
id("configuration")
}
android {
namespace = "com.tangem.lib.crypto"
}
dependencies {
/** Coroutines */
implementation(Library.coroutine)
}
implementation(deps.kotlin.coroutines)
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
/** SDK */
implementation(tangemDeps.blockchain)
/** Core */
implementation(projects.core.utils)
/** Libs */
implementation(projects.libs.blockchainSdk)
}

View file

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.lib.crypto" />

View file

@ -0,0 +1,159 @@
package com.tangem.lib.crypto
import com.tangem.blockchain.blockchains.xrp.XrpAddressService
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.compatibility.l2BlockchainsList
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.minimalAmount
import com.tangem.lib.crypto.converter.XrpTaggedAddressConverter
import com.tangem.lib.crypto.models.XrpTaggedAddress
import java.math.BigDecimal
/**
* !!!IMPORTANT!!!
* Methods for working with different blockchains
* All methods are depend on specific blockchain or check for specific blockchain
*
* Temporary solution for domain specific logic for Blockchain.
* Instead of creating repositories and unnecessary and overkill use cases
*/
object BlockchainUtils {
private const val XRP_X_ADDRESS = 'X'
/** Decodes XRP Blockchain address */
fun decodeRippleXAddress(xAddress: String, blockchainId: String): XrpTaggedAddress? {
return if (blockchainId == Blockchain.XRP.id && xAddress.firstOrNull() == XRP_X_ADDRESS) {
val decodedAddress = XrpAddressService.decodeXAddress(xAddress)
return decodedAddress?.let(XrpTaggedAddressConverter()::convert)
} else {
null
}
}
/** If current [networkId] is Bitcoin */
fun isBitcoin(blockchainId: String): Boolean {
val blockchain = Blockchain.fromId(blockchainId)
return blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet
}
/** If current [networkId] is use custom fee */
fun isUseBitcoinFeeConverter(blockchainId: String): Boolean {
val blockchain = Blockchain.fromId(blockchainId)
return isBitcoin(blockchainId) || blockchain == Blockchain.Fact0rn
}
/** If current [blockchainId] is Tezos */
fun isTezos(blockchainId: String): Boolean {
val blockchain = Blockchain.fromId(blockchainId)
return blockchain == Blockchain.Tezos
}
fun isCardano(blockchainId: String): Boolean {
val blockchain = Blockchain.fromId(blockchainId)
return blockchain == Blockchain.Cardano
}
/** If current [blockchainId] is BeaconChain */
fun isBeaconChain(blockchainId: String): Boolean {
val blockchain = Blockchain.fromId(blockchainId)
return blockchain == Blockchain.Binance || blockchain == Blockchain.BinanceTestnet
}
/** If current [blockchainId] is Polygon */
fun isPolygonChain(blockchainId: String): Boolean {
val blockchain = Blockchain.fromId(blockchainId)
return blockchain == Blockchain.Polygon || blockchain == Blockchain.PolygonTestnet
}
fun isTron(blockchainId: String): Boolean {
val blockchain = Blockchain.fromId(blockchainId)
return blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet
}
fun isSupportedNetworkId(blockchainId: String, excludedBlockchains: ExcludedBlockchains): Boolean {
val blockchain = Blockchain.fromNetworkId(blockchainId)
return blockchain != null && blockchain !in excludedBlockchains
}
fun isArbitrum(blockchainId: String): Boolean {
val blockchain = Blockchain.fromId(blockchainId)
return blockchain == Blockchain.Arbitrum
}
fun isSolana(blockchainId: String): Boolean {
val blockchain = Blockchain.fromId(blockchainId)
return blockchain == Blockchain.Solana
}
fun isPolkadot(blockchainId: String): Boolean {
val blockchain = Blockchain.fromId(blockchainId)
return blockchain == Blockchain.Polkadot || blockchain == Blockchain.PolkadotTestnet
}
fun isCosmos(blockchainId: String): Boolean {
val blockchain = Blockchain.fromId(blockchainId)
return blockchain == Blockchain.Cosmos || blockchain == Blockchain.CosmosTestnet
}
fun isBSC(blockchainId: String): Boolean {
val blockchain = Blockchain.fromId(blockchainId)
return blockchain == Blockchain.BSC || blockchain == Blockchain.BSCTestnet
}
data class BlockchainInfo(
val blockchainId: String,
val name: String,
val protocolName: String,
)
fun getNetworkInfo(networkId: String): BlockchainInfo? {
val blockchain = Blockchain.fromNetworkId(networkId) ?: return null
return BlockchainInfo(
blockchainId = blockchain.id,
name = getNetworkNameWithoutTestnet(blockchain),
protocolName = getNetworkStandardName(blockchain),
)
}
fun isL2Network(networkId: String): Boolean {
val blockchain = Blockchain.fromNetworkId(networkId) ?: return false
return l2BlockchainsList.contains(blockchain)
}
fun getTezosThreshold(): BigDecimal = Blockchain.Tezos.minimalAmount()
/**
* Blockchains not affecting total balance counting on errors
*/
fun isIncludeToBalanceOnError(blockchainId: String): Boolean {
val blockchain = Blockchain.fromId(blockchainId)
return when (blockchain) {
Blockchain.Binance, Blockchain.BinanceTestnet -> true
else -> false
}
}
private fun getNetworkStandardName(blockchain: Blockchain): String {
return when (blockchain) {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ERC20"
Blockchain.BSC, Blockchain.BSCTestnet -> "BEP20"
Blockchain.Binance, Blockchain.BinanceTestnet -> "BEP2"
Blockchain.Tron, Blockchain.TronTestnet -> "TRC20"
Blockchain.TON -> "TON"
else -> ""
}
}
private fun getNetworkNameWithoutTestnet(blockchain: Blockchain): String {
return blockchain.fullName.replace(oldValue = " Testnet", newValue = "")
}
fun isTokenBetaFunctionality(blockchainId: String): Boolean {
val blockchain = Blockchain.fromId(blockchainId)
return blockchain == Blockchain.Kaspa
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.lib.crypto
import com.tangem.lib.crypto.models.Currency
interface DerivationManager {
/**
* Derives missing blockchain and returns flag true if did it successfully
*
* @param currency to derive (Native token or not)
*/
suspend fun deriveMissingBlockchains(currency: Currency): Boolean
/**
* Checks that given [networkId] has derivations
*/
fun hasDerivation(networkId: String): Boolean
}

View file

@ -1,50 +1,40 @@
package com.tangem.lib.crypto
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.ProxyAmount
import com.tangem.lib.crypto.models.ProxyNetworkInfo
import com.tangem.lib.crypto.models.transactions.SendTxResult
import java.math.BigDecimal
import com.tangem.blockchain.common.Amount
import com.tangem.lib.crypto.models.*
import java.math.BigInteger
interface TransactionManager {
@Throws(IllegalStateException::class)
suspend fun sendApproveTransaction(
networkId: String,
feeAmount: BigDecimal,
estimatedGas: Int,
destinationAddress: String,
dataToSign: String,
): SendTxResult
/**
* Get fee
*
* @param networkId network id of blockchain
* @param amountToSend amount
* @param currencyToSend currency to send in tx
* @param destinationAddress address to send tx
* @param increaseBy percents in format 125 = 25%
* @param data data for tx
* @param derivationPath derivation path
* @return
*/
@Suppress("LongParameterList")
@Throws(IllegalStateException::class)
suspend fun sendTransaction(
networkId: String,
amountToSend: BigDecimal,
feeAmount: BigDecimal,
estimatedGas: Int,
destinationAddress: String,
dataToSign: String,
isSwap: Boolean,
currencyToSend: Currency,
): SendTxResult
@Throws(IllegalStateException::class)
suspend fun getFee(
networkId: String,
amountToSend: BigDecimal,
amountToSend: Amount,
currencyToSend: Currency,
destinationAddress: String,
): ProxyAmount
increaseBy: Int?,
data: String?,
derivationPath: String?,
): ProxyFees
@Throws(IllegalStateException::class)
fun getNativeTokenDecimals(networkId: String): Int
suspend fun getFeeForGas(networkId: String, gas: BigInteger, derivationPath: String?): ProxyFees
@Throws(IllegalStateException::class)
suspend fun updateWalletManager(networkId: String)
fun calculateFee(networkId: String, gasPrice: String, estimatedGas: Int): BigDecimal
suspend fun updateWalletManager(networkId: String, derivationPath: String?)
/**
* In app blockchain id, actual in blockchain sdk, not the same as networkId

View file

@ -1,8 +1,6 @@
package com.tangem.lib.crypto
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.ProxyAmount
import com.tangem.lib.crypto.models.ProxyFiatCurrency
/**
* Provider for user tokens data
@ -10,62 +8,24 @@ import com.tangem.lib.crypto.models.ProxyFiatCurrency
interface UserWalletManager {
/**
* Returns all user tokens (merged from local and backend)
*/
suspend fun getUserTokens(networkId: String): List<Currency>
fun getNativeTokenForNetwork(networkId: String): Currency
/**
* Returns user walletId
* Returns user walletId or empty string
*/
fun getWalletId(): String
/**
* Checks that token added to user wallet
*
* @param currency to receive referral payments
*/
suspend fun isTokenAdded(currency: Currency): Boolean
/**
* Adds token to wallet if its not
*
* @param currency to add to wallet
*/
@Throws(IllegalStateException::class)
fun addToken(currency: Currency)
suspend fun hideAllTokens()
/**
* Returns wallet public address for token
*
* @param networkId for currency
* @param derivationPath if null uses default
*/
@Throws(IllegalStateException::class)
fun getWalletAddress(networkId: String): String
/**
* Return balances from wallet found by networkId
*
* @param networkId
* @return map of <Symbol, [ProxyAmount]>
*/
@Throws(IllegalStateException::class)
fun getCurrentWalletTokensBalance(networkId: String): Map<String, ProxyAmount>
fun getNativeTokenBalance(networkId: String): ProxyAmount?
/**
* @param networkId
* @return currency name
*/
fun getNetworkCurrency(networkId: String): String
/**
* Returns selected app currency
*/
fun getUserAppCurrency(): ProxyFiatCurrency
suspend fun getWalletAddress(networkId: String, derivationPath: String?): String
@Throws(IllegalStateException::class)
fun getLastTransactionHash(networkId: String): String?
suspend fun getNativeTokenBalance(networkId: String, derivationPath: String?): ProxyAmount?
@Throws(IllegalStateException::class)
suspend fun getLastTransactionHash(networkId: String, derivationPath: String?): String?
}

View file

@ -0,0 +1,15 @@
package com.tangem.lib.crypto.converter
import com.tangem.lib.crypto.models.XrpTaggedAddress
import com.tangem.utils.converter.Converter
import com.tangem.blockchain.blockchains.xrp.XrpTaggedAddress as BlockchainXrpTaggedAddress
internal class XrpTaggedAddressConverter : Converter<BlockchainXrpTaggedAddress, XrpTaggedAddress> {
override fun convert(value: BlockchainXrpTaggedAddress): XrpTaggedAddress {
return XrpTaggedAddress(
address = value.address,
destinationTag = value.destinationTag,
)
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.lib.crypto.models
/**
* Analytics data for send events in analytics engine
*
* @property feeType type of fee (min,max,normal)
* @property tokenSymbol symbol
* @property permissionType optional parameter used for type tx approve
*/
data class AnalyticsData(
val feeType: String,
val tokenSymbol: String,
val permissionType: String? = null,
)

View file

@ -0,0 +1,20 @@
package com.tangem.lib.crypto.models
import java.math.BigDecimal
/**
* Tx data for create and make approve transaction
*
* @property networkId id of network
* @property feeAmount amount of fee
* @property gasLimit gasLimit for given tx
* @property destinationAddress address to send tx
* @property dataToSign data to sing with signer
*/
data class ApproveTxData(
val networkId: String,
val feeAmount: BigDecimal,
val gasLimit: Int,
val destinationAddress: String,
val dataToSign: String,
)

View file

@ -13,4 +13,11 @@ data class ProxyAmount(
val currencySymbol: String,
var value: BigDecimal,
val decimals: Int,
)
) {
companion object {
fun empty(): ProxyAmount {
return ProxyAmount("", BigDecimal.ZERO, 0)
}
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.lib.crypto.models
import java.math.BigDecimal
import java.math.BigInteger
sealed interface ProxyFee {
val gasLimit: BigInteger
val fee: ProxyAmount
data class Common(
override val gasLimit: BigInteger,
override val fee: ProxyAmount,
) : ProxyFee
data class CardanoToken(
override val gasLimit: BigInteger,
override val fee: ProxyAmount,
val minAdaValue: BigDecimal,
) : ProxyFee
data class Filecoin(
override val gasLimit: BigInteger,
override val fee: ProxyAmount,
val gasPremium: Long,
) : ProxyFee
data class Sui(
override val gasLimit: BigInteger,
override val fee: ProxyAmount,
val gasPrice: Long,
val gasBudget: Long,
) : ProxyFee
}

View file

@ -0,0 +1,14 @@
package com.tangem.lib.crypto.models
sealed class ProxyFees {
data class MultipleFees(
val minFee: ProxyFee,
val normalFee: ProxyFee,
val priorityFee: ProxyFee,
) : ProxyFees()
data class SingleFee(
val singleFee: ProxyFee,
) : ProxyFees()
}

View file

@ -3,5 +3,4 @@ package com.tangem.lib.crypto.models
data class ProxyNetworkInfo(
val name: String,
val blockchainId: String,
val blockchainCurrency: String,
)

View file

@ -0,0 +1,25 @@
package com.tangem.lib.crypto.models
import java.math.BigDecimal
/**
* Tx data for create and make transaction
*
* @property networkId id of network
* @property feeAmount amount of fee
* @property gasLimit gasLimit for given tx
* @property destinationAddress address to send tx
* @property dataToSign data to sing with signer
* @property amountToSend amount of tx
* @property currencyToSend currency for tx
*/
// TODO split to cex,dex
data class SwapTxData(
val networkId: String,
val feeAmount: BigDecimal,
val gasLimit: Int,
val destinationAddress: String,
val dataToSign: String,
val amountToSend: BigDecimal,
val currencyToSend: Currency,
)

View file

@ -0,0 +1,6 @@
package com.tangem.lib.crypto.models
data class XrpTaggedAddress(
val address: String,
val destinationTag: Long?,
)

View file

@ -6,5 +6,6 @@ sealed interface SendTxResult {
object UserCancelledError : SendTxResult
data class TangemSdkError(val code: Int, val cause: Throwable?) : SendTxResult
data class BlockchainSdkError(val code: Int, val cause: Throwable?) : SendTxResult
data class NetworkError(val ex: Exception? = null) : SendTxResult
data class UnknownError(val ex: Exception? = null) : SendTxResult
}

1
libs/tangem-sdk-api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,36 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.kotlin.serialization)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.legacy"
}
dependencies {
implementation(projects.common)
implementation(projects.domain.models)
implementation(projects.domain.card)
implementation(projects.domain.legacy)
implementation(projects.domain.wallets.models)
implementation(projects.domain.visa.models)
implementation(projects.core.res)
/** Tangem libraries */
implementation(tangemDeps.card.core)
implementation(tangemDeps.card.android) {
exclude(module = "joda-time")
}
/** Other libraries */
implementation(deps.timber)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,17 @@
package com.tangem.sdk.api
import androidx.activity.ComponentActivity
import com.tangem.TangemSdk
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.extensions.init
import java.lang.ref.WeakReference
class BackupServiceHolder {
lateinit var backupService: WeakReference<BackupService>
private set
fun createAndSetService(tangemSdk: TangemSdk, activity: ComponentActivity) {
backupService = WeakReference(BackupService.init(tangemSdk, activity))
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.sdk.api
import com.tangem.common.card.Card
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.KeyWalletPublicKey
import com.tangem.operations.CommandResponse
import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.derivation.ExtendedPublicKeysMap
data class CreateProductWalletTaskResponse(
val card: CardDTO,
val derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap> = mapOf(),
val primaryCard: PrimaryCard? = null,
) : CommandResponse {
constructor(
card: Card,
derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap> = mapOf(),
primaryCard: PrimaryCard? = null,
) : this(
card = CardDTO(card),
derivedKeys = derivedKeys,
primaryCard = primaryCard,
)
}

View file

@ -0,0 +1,162 @@
package com.tangem.sdk.api
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.tangem.Message
import com.tangem.common.CompletionResult
import com.tangem.common.KeyPair
import com.tangem.common.SuccessResponse
import com.tangem.common.authentication.keystore.KeystoreManager
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.UserCodeRequestPolicy
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.services.secure.SecureStorage
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.preflightread.PreflightReadFilter
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.sdk.api.visa.VisaCardActivationResponse
import com.tangem.sdk.api.visa.VisaCardActivationTaskMode
@Suppress("TooManyFunctions")
interface TangemSdkManager {
val canUseBiometry: Boolean
val needEnrollBiometrics: Boolean
val keystoreManager: KeystoreManager
val secureStorage: SecureStorage
val userCodeRequestPolicy: UserCodeRequestPolicy
suspend fun checkCanUseBiometry(awaitInitialization: Boolean = true): Boolean
suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean = true): Boolean
suspend fun scanProduct(
cardId: String? = null,
messageRes: Int? = null,
allowsRequestAccessCodeFromRepository: Boolean = false,
): CompletionResult<ScanResponse>
suspend fun createProductWallet(
scanResponse: ScanResponse,
shouldReset: Boolean = false,
): CompletionResult<CreateProductWalletTaskResponse>
// Wallet2 specific
suspend fun importWallet(
scanResponse: ScanResponse,
mnemonic: String,
passphrase: String?,
shouldReset: Boolean,
): CompletionResult<CreateProductWalletTaskResponse>
suspend fun derivePublicKeys(
cardId: String?,
derivations: Map<ByteArrayKey, List<DerivationPath>>,
preflightReadFilter: PreflightReadFilter?,
): CompletionResult<DerivationTaskResponse>
suspend fun deriveExtendedPublicKey(
cardId: String?,
walletPublicKey: ByteArray,
derivation: DerivationPath,
): CompletionResult<ExtendedPublicKey>
suspend fun resetToFactorySettings(
cardId: String,
allowsRequestAccessCodeFromRepository: Boolean,
): CompletionResult<Boolean>
suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult<Boolean>
suspend fun saveAccessCode(accessCode: String, cardsIds: Set<String>): CompletionResult<Unit>
suspend fun deleteSavedUserCodes(cardsIds: Set<String>): CompletionResult<Unit>
suspend fun clearSavedUserCodes(): CompletionResult<Unit>
suspend fun setPasscode(cardId: String?): CompletionResult<SuccessResponse>
suspend fun setAccessCode(cardId: String?): CompletionResult<SuccessResponse>
suspend fun setLongTap(cardId: String?): CompletionResult<SuccessResponse>
suspend fun setAccessCodeRecoveryEnabled(cardId: String?, enabled: Boolean): CompletionResult<SuccessResponse>
suspend fun scanCard(
cardId: String? = null,
allowRequestAccessCodeFromRepository: Boolean = false,
): CompletionResult<CardDTO>
@Deprecated(
"com.tangem.sdk.api.TangemSdkManager shouldn't run custom tasks. " +
"All of them should be specified in com.tangem.sdk.api.TangemSdkManager certain methods.",
)
suspend fun <T> runTaskAsync(
runnable: CardSessionRunnable<T>,
preflightReadFilter: PreflightReadFilter?,
cardId: String? = null,
initialMessage: Message? = null,
accessCode: String? = null,
@DrawableRes iconScanRes: Int? = null,
): CompletionResult<T>
@Suppress("MagicNumber")
fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse?)
@Deprecated("com.tangem.sdk.api.TangemSdkManager shouldn't returns a string from resources")
fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String
fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy)
// region Twin-specific
suspend fun finalizeTwin(
secondCardPublicKey: ByteArray,
issuerKeyPair: KeyPair,
cardId: String,
initialMessage: Message,
): CompletionResult<ScanResponse>
suspend fun createFirstTwinWallet(cardId: String, initialMessage: Message): CompletionResult<CreateWalletResponse>
@Suppress("LongParameterList")
suspend fun createSecondTwinWallet(
firstPublicKey: String,
firstCardId: String,
issuerKeys: KeyPair,
preparingMessage: Message,
creatingWalletMessage: Message,
initialMessage: Message,
): CompletionResult<CreateWalletResponse>
fun changeProductType(isRing: Boolean)
fun clearProductType()
// endregion
// region Visa-specific
suspend fun activateVisaCard(
mode: VisaCardActivationTaskMode,
activationInput: VisaActivationInput,
): CompletionResult<VisaCardActivationResponse>
suspend fun visaCustomerWalletApprove(
visaDataForApprove: VisaDataForApprove,
): CompletionResult<VisaSignedDataByCustomerWallet>
// endregion
}

View file

@ -0,0 +1,49 @@
package com.tangem.sdk.api
import androidx.annotation.StringRes
import com.tangem.common.core.TangemError
import com.tangem.legacy.R
interface TapErrors
interface ArgError {
val args: List<Any>?
}
interface MultiMessageError : TapErrors {
val errorList: List<TapError>
val builder: (List<String>) -> String
}
sealed class TapError(
@StringRes val messageResource: Int,
override val args: List<Any>? = null,
) : Throwable(), TapErrors, ArgError {
object UnknownError : TapError(R.string.send_error_unknown)
open class CustomError(val customMessage: String) : TapError(R.string.common_custom_string, listOf(customMessage))
object NoInternetConnection : TapError(R.string.wallet_notification_no_internet)
sealed class WalletManager {
class NoAccountError(amountToCreateAccount: String) : CustomError(amountToCreateAccount)
class InternalError(message: String) : CustomError(message)
object BlockchainIsUnreachableTryLater : TapError(R.string.wallet_balance_blockchain_unreachable_try_later)
}
}
sealed class TapSdkError(override val messageResId: Int?) : TangemError(code = 50100) {
override var customMessage: String = code.toString()
object CardForDifferentApp : TapSdkError(R.string.alert_unsupported_card)
object CardNotSupportedByRelease : TapSdkError(R.string.error_wrong_card_type)
}
fun TapErrors.assembleErrors(): MutableList<Pair<Int, List<Any>?>> {
val idList = mutableListOf<Pair<Int, List<Any>?>>()
when (this) {
is MultiMessageError -> this.errorList.forEach { idList.addAll(it.assembleErrors()) }
is TapError -> idList.add(Pair(this.messageResource, this.args))
}
return idList
}

View file

@ -0,0 +1,9 @@
package com.tangem.sdk.api.visa
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.visa.model.VisaSignedActivationDataByCardWallet
data class VisaCardActivationResponse(
val signedActivationData: VisaSignedActivationDataByCardWallet,
val newCardDTO: CardDTO,
)

View file

@ -0,0 +1,20 @@
package com.tangem.sdk.api.visa
import com.tangem.domain.visa.model.VisaAuthChallenge
import com.tangem.domain.visa.model.VisaDataToSignByCardWallet
sealed class VisaCardActivationTaskMode {
/**
* Full activation process with getting remote activation status.
*/
data class Full(
val accessCode: String,
val authorizationChallenge: VisaAuthChallenge.Card,
) : VisaCardActivationTaskMode()
/**
* Activation process with only sign data by card wallet.
* This is used when activation process was interrupted and we need to finish it.
*/
data class SignOnly(val dataToSignByCardWallet: VisaDataToSignByCardWallet) : VisaCardActivationTaskMode()
}

1
libs/visa/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,35 @@
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
android {
namespace = "com.tangem.libs.visa"
}
dependencies {
/** Project */
implementation(projects.core.utils)
implementation(projects.core.datasource)
implementation(projects.data.common)
/** Libs - Network */
implementation(deps.moshi.kotlin)
implementation(deps.okHttp)
implementation(deps.okHttp.prettyLogging)
implementation(deps.retrofit)
implementation(deps.retrofit.moshi)
kaptForObfuscatingVariants(deps.moshi.kotlin.codegen)
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
/** Libs - Other */
implementation(deps.web3j.core)
implementation(deps.kotlin.coroutines)
implementation(deps.arrow.fx)
implementation(deps.jodatime)
}

View file

@ -0,0 +1,267 @@
package com.tangem.lib.visa;
import org.web3j.abi.EventEncoder;
import org.web3j.abi.TypeReference;
import org.web3j.abi.datatypes.*;
import org.web3j.abi.datatypes.generated.Uint256;
import org.web3j.abi.datatypes.generated.Uint8;
import org.web3j.crypto.Credentials;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameter;
import org.web3j.protocol.core.RemoteFunctionCall;
import org.web3j.protocol.core.methods.request.EthFilter;
import org.web3j.protocol.core.methods.response.BaseEventResponse;
import org.web3j.protocol.core.methods.response.Log;
import org.web3j.protocol.core.methods.response.TransactionReceipt;
import org.web3j.tx.Contract;
import org.web3j.tx.TransactionManager;
import org.web3j.tx.gas.ContractGasProvider;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import io.reactivex.Flowable;
/**
* <p>Auto generated code.
* <p><strong>Do not modify!</strong>
* <p>Please use the <a href="https://docs.web3j.io/command_line.html">web3j command line tools</a>,
* or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the
* <a href="https://github.com/web3j/web3j/tree/master/codegen">codegen module</a> to update.
*
* <p>Generated with web3j version 1.5.2.
*/
@SuppressWarnings("rawtypes")
class ERC20 extends Contract {
public static final String BINARY = "Bin file was not provided";
public static final String FUNC_ALLOWANCE = "allowance";
public static final String FUNC_APPROVE = "approve";
public static final String FUNC_BALANCEOF = "balanceOf";
public static final String FUNC_DECIMALS = "decimals";
public static final String FUNC_NAME = "name";
public static final String FUNC_SYMBOL = "symbol";
public static final String FUNC_TOTALSUPPLY = "totalSupply";
public static final String FUNC_TRANSFER = "transfer";
public static final String FUNC_TRANSFERFROM = "transferFrom";
public static final Event APPROVAL_EVENT = new Event("Approval",
Arrays.asList(new TypeReference<Address>(true) {
}, new TypeReference<Address>(true) {
}, new TypeReference<Uint256>() {
}));
public static final Event TRANSFER_EVENT = new Event("Transfer",
Arrays.asList(new TypeReference<Address>(true) {
}, new TypeReference<Address>(true) {
}, new TypeReference<Uint256>() {
}));
@Deprecated
protected ERC20(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) {
super(BINARY, contractAddress, web3j, credentials, gasPrice, gasLimit);
}
protected ERC20(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) {
super(BINARY, contractAddress, web3j, credentials, contractGasProvider);
}
@Deprecated
protected ERC20(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) {
super(BINARY, contractAddress, web3j, transactionManager, gasPrice, gasLimit);
}
protected ERC20(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) {
super(BINARY, contractAddress, web3j, transactionManager, contractGasProvider);
}
public static List<ApprovalEventResponse> getApprovalEvents(TransactionReceipt transactionReceipt) {
List<EventValuesWithLog> valueList = staticExtractEventParametersWithLog(APPROVAL_EVENT, transactionReceipt);
ArrayList<ApprovalEventResponse> responses = new ArrayList<ApprovalEventResponse>(valueList.size());
for (EventValuesWithLog eventValues : valueList) {
ApprovalEventResponse typedResponse = new ApprovalEventResponse();
typedResponse.log = eventValues.getLog();
typedResponse.owner = (String) eventValues.getIndexedValues().get(0).getValue();
typedResponse.spender = (String) eventValues.getIndexedValues().get(1).getValue();
typedResponse.value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue();
responses.add(typedResponse);
}
return responses;
}
public static ApprovalEventResponse getApprovalEventFromLog(Log log) {
EventValuesWithLog eventValues = staticExtractEventParametersWithLog(APPROVAL_EVENT, log);
ApprovalEventResponse typedResponse = new ApprovalEventResponse();
typedResponse.log = log;
typedResponse.owner = (String) eventValues.getIndexedValues().get(0).getValue();
typedResponse.spender = (String) eventValues.getIndexedValues().get(1).getValue();
typedResponse.value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue();
return typedResponse;
}
@Deprecated
public static ERC20 load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) {
return new ERC20(contractAddress, web3j, credentials, gasPrice, gasLimit);
}
@Deprecated
public static ERC20 load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) {
return new ERC20(contractAddress, web3j, transactionManager, gasPrice, gasLimit);
}
public static List<TransferEventResponse> getTransferEvents(TransactionReceipt transactionReceipt) {
List<EventValuesWithLog> valueList = staticExtractEventParametersWithLog(TRANSFER_EVENT, transactionReceipt);
ArrayList<TransferEventResponse> responses = new ArrayList<TransferEventResponse>(valueList.size());
for (EventValuesWithLog eventValues : valueList) {
TransferEventResponse typedResponse = new TransferEventResponse();
typedResponse.log = eventValues.getLog();
typedResponse.from = (String) eventValues.getIndexedValues().get(0).getValue();
typedResponse.to = (String) eventValues.getIndexedValues().get(1).getValue();
typedResponse.value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue();
responses.add(typedResponse);
}
return responses;
}
public static TransferEventResponse getTransferEventFromLog(Log log) {
EventValuesWithLog eventValues = staticExtractEventParametersWithLog(TRANSFER_EVENT, log);
TransferEventResponse typedResponse = new TransferEventResponse();
typedResponse.log = log;
typedResponse.from = (String) eventValues.getIndexedValues().get(0).getValue();
typedResponse.to = (String) eventValues.getIndexedValues().get(1).getValue();
typedResponse.value = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue();
return typedResponse;
}
public Flowable<TransferEventResponse> transferEventFlowable(EthFilter filter) {
return web3j.ethLogFlowable(filter).map(log -> getTransferEventFromLog(log));
}
public Flowable<TransferEventResponse> transferEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) {
EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress());
filter.addSingleTopic(EventEncoder.encode(TRANSFER_EVENT));
return transferEventFlowable(filter);
}
public static ERC20 load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) {
return new ERC20(contractAddress, web3j, credentials, contractGasProvider);
}
public static ERC20 load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) {
return new ERC20(contractAddress, web3j, transactionManager, contractGasProvider);
}
public Flowable<ApprovalEventResponse> approvalEventFlowable(EthFilter filter) {
return web3j.ethLogFlowable(filter).map(log -> getApprovalEventFromLog(log));
}
public Flowable<ApprovalEventResponse> approvalEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) {
EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress());
filter.addSingleTopic(EventEncoder.encode(APPROVAL_EVENT));
return approvalEventFlowable(filter);
}
public RemoteFunctionCall<BigInteger> allowance(String owner, String spender) {
final Function function = new Function(FUNC_ALLOWANCE,
Arrays.asList(new Address(160, owner),
new Address(160, spender)),
List.of(new TypeReference<Uint256>() {
}));
return executeRemoteCallSingleValueReturn(function, BigInteger.class);
}
public RemoteFunctionCall<TransactionReceipt> approve(String spender, BigInteger value) {
final Function function = new Function(
FUNC_APPROVE,
Arrays.asList(new Address(160, spender),
new Uint256(value)),
Collections.emptyList());
return executeRemoteCallTransaction(function);
}
public RemoteFunctionCall<BigInteger> balanceOf(String account) {
final Function function = new Function(FUNC_BALANCEOF,
List.of(new Address(160, account)),
List.of(new TypeReference<Uint256>() {
}));
return executeRemoteCallSingleValueReturn(function, BigInteger.class);
}
public RemoteFunctionCall<BigInteger> decimals() {
final Function function = new Function(FUNC_DECIMALS,
List.of(),
List.of(new TypeReference<Uint8>() {
}));
return executeRemoteCallSingleValueReturn(function, BigInteger.class);
}
public RemoteFunctionCall<String> name() {
final Function function = new Function(FUNC_NAME,
List.of(),
List.of(new TypeReference<Utf8String>() {
}));
return executeRemoteCallSingleValueReturn(function, String.class);
}
public RemoteFunctionCall<String> symbol() {
final Function function = new Function(FUNC_SYMBOL,
List.of(),
List.of(new TypeReference<Utf8String>() {
}));
return executeRemoteCallSingleValueReturn(function, String.class);
}
public RemoteFunctionCall<BigInteger> totalSupply() {
final Function function = new Function(FUNC_TOTALSUPPLY,
List.of(),
List.of(new TypeReference<Uint256>() {
}));
return executeRemoteCallSingleValueReturn(function, BigInteger.class);
}
public RemoteFunctionCall<TransactionReceipt> transfer(String to, BigInteger value) {
final Function function = new Function(
FUNC_TRANSFER,
Arrays.asList(new Address(160, to),
new Uint256(value)),
Collections.emptyList());
return executeRemoteCallTransaction(function);
}
public RemoteFunctionCall<TransactionReceipt> transferFrom(String from, String to, BigInteger value) {
final Function function = new Function(
FUNC_TRANSFERFROM,
Arrays.asList(new Address(160, from),
new Address(160, to),
new Uint256(value)),
Collections.emptyList());
return executeRemoteCallTransaction(function);
}
public static class ApprovalEventResponse extends BaseEventResponse {
public String owner;
public String spender;
public BigInteger value;
}
public static class TransferEventResponse extends BaseEventResponse {
public String from;
public String to;
public BigInteger value;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,160 @@
package com.tangem.lib.visa
import arrow.fx.coroutines.parZip
import com.tangem.lib.visa.model.VisaContractInfo
import com.tangem.lib.visa.model.VisaContractInfo.*
import com.tangem.lib.visa.utils.toBigDecimal
import com.tangem.lib.visa.utils.toInstant
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import org.joda.time.Instant
import org.web3j.protocol.Web3j
import org.web3j.tx.TransactionManager
import org.web3j.tx.gas.ContractGasProvider
internal class DefaultVisaContractInfoProvider(
private val web3j: Web3j,
private val transactionManager: TransactionManager,
private val gasProvider: ContractGasProvider,
private val bridgeProcessorAddress: String,
private val paymentAccountRegistryAddress: String,
private val dispatchers: CoroutineDispatcherProvider,
) : VisaContractInfoProvider {
override suspend fun getContractInfo(walletAddress: String): VisaContractInfo {
return parZip(
dispatchers.io,
{ loadPaymentAccount(walletAddress) },
{ loadPaymentTokenInfo() },
{ paymentAccount, paymentToken ->
fetchBalancesAndLimits(paymentAccount, paymentToken)
},
)
}
private fun loadPaymentAccount(walletAddress: String): TangemPaymentAccount {
val paymentAccountRegistry = TangemPaymentAccountRegistry.load(
/* contractAddress = */ paymentAccountRegistryAddress,
/* web3j = */ web3j,
/* transactionManager = */ transactionManager,
/* contractGasProvider = */ gasProvider,
)
val paymentAccountAddress = paymentAccountRegistry.paymentAccountByCard(walletAddress).send()
return TangemPaymentAccount.load(paymentAccountAddress, web3j, transactionManager, gasProvider)
}
private fun loadPaymentTokenInfo(): PaymentTokenInfo {
val tangemBridgeProcessor = TangemBridgeProcessor.load(
/* contractAddress = */ bridgeProcessorAddress,
/* web3j = */ web3j,
/* transactionManager = */ transactionManager,
/* contractGasProvider = */ gasProvider,
)
val paymentTokenContractAddress = tangemBridgeProcessor.paymentToken().send()
val paymentTokenContract = ERC20.load(paymentTokenContractAddress, web3j, transactionManager, gasProvider)
val paymentTokenDecimals = paymentTokenContract.decimals().send()
return PaymentTokenInfo(
decimals = paymentTokenDecimals.toInt(),
contract = paymentTokenContract,
)
}
private suspend fun fetchBalancesAndLimits(
paymentAccount: TangemPaymentAccount,
paymentToken: PaymentTokenInfo,
): VisaContractInfo = parZip(
dispatchers.io,
{ fetchToken(paymentAccount) },
{ fetchBalances(paymentAccount, paymentToken) },
{ fetchLimits(paymentAccount, paymentToken) },
{ token, balances, (oldLimit, newLimit, changeDate) ->
VisaContractInfo(token, balances, oldLimit, newLimit, changeDate)
},
)
private suspend fun fetchToken(paymentAccount: TangemPaymentAccount): Token {
val paymentTokenContractAddress = paymentAccount.paymentToken().send()
val paymentTokenContract = ERC20.load(paymentTokenContractAddress, web3j, transactionManager, gasProvider)
return parZip(
dispatchers.io,
{ paymentTokenContract.name().send() },
{ paymentTokenContract.symbol().send() },
{ paymentTokenContract.decimals().send() },
) { name, symbol, decimals ->
Token(
name = name,
symbol = symbol,
decimals = decimals.toInt(),
address = paymentTokenContractAddress,
)
}
}
private suspend fun fetchBalances(paymentAccount: TangemPaymentAccount, paymentToken: PaymentTokenInfo): Balances {
return parZip(
dispatchers.io,
{ paymentToken.contract.balanceOf(paymentAccount.contractAddress).send() },
{ paymentAccount.verifiedBalance().send() },
{ paymentAccount.availableForPayment().send() },
{ paymentAccount.availableForWithdrawal().send() },
{ paymentAccount.availableForDebtPayment().send() },
{ paymentAccount.blockedAmount().send() },
{ paymentAccount.debtAmount().send() },
{ paymentAccount.pendingRefundTotal().send() },
) { total, verified, payment, withdrawal, debtPayment, blocked, debt, refund ->
val decimals = paymentToken.decimals
Balances(
total = total.toBigDecimal(decimals),
verified = verified.toBigDecimal(decimals),
available = Balances.Available(
forPayment = payment.toBigDecimal(decimals),
forWithdrawal = withdrawal.toBigDecimal(decimals),
forDebtPayment = debtPayment.toBigDecimal(decimals),
),
blocked = blocked.toBigDecimal(decimals),
debt = debt.toBigDecimal(decimals),
pendingRefund = refund.toBigDecimal(decimals),
)
}
}
private fun fetchLimits(
paymentAccount: TangemPaymentAccount,
paymentToken: PaymentTokenInfo,
): Triple<Limits, Limits, Instant> {
val (
oldLimit,
newLimit,
changeDateSeconds,
) = paymentAccount.limits().send()
return Triple(
first = getLimits(oldLimit, paymentToken),
second = getLimits(newLimit, paymentToken),
third = changeDateSeconds.toInstant(),
)
}
private fun getLimits(limit: TangemPaymentAccount.Limits, paymentToken: PaymentTokenInfo): Limits = Limits(
spendLimit = limit._01_spendLimit.toLimit(paymentToken.decimals),
noOtpLimit = limit._02_noOtpSpendLimit.toLimit(paymentToken.decimals),
singleTransactionLimit = limit._00_singleTransactionLimit.toBigDecimal(paymentToken.decimals),
expirationDate = limit._03_spendLimitsTimer.expireTimestamp.toInstant(),
spendPeriodSeconds = limit._04_spendLimitsPeriod,
)
private fun TangemPaymentAccount.Limit.toLimit(decimals: Int): Limits.Limit {
return Limits.Limit(
limit = _00_limit.toBigDecimal(decimals),
spent = _01_spent.toBigDecimal(decimals),
)
}
private data class PaymentTokenInfo(
val decimals: Int,
val contract: ERC20,
)
}

View file

@ -0,0 +1,94 @@
package com.tangem.lib.visa
import com.ihsanbal.logging.Level
import com.ihsanbal.logging.LoggingInterceptor
import com.tangem.lib.visa.model.VisaContractInfo
import com.tangem.lib.visa.utils.Constants
import com.tangem.lib.visa.utils.Constants.NETWORK_LOGS_TAG
import com.tangem.lib.visa.utils.toHexString
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import okhttp3.OkHttpClient
import org.web3j.crypto.Credentials
import org.web3j.protocol.Web3j
import org.web3j.protocol.http.HttpService
import org.web3j.tx.FastRawTransactionManager
import org.web3j.tx.TransactionManager
import org.web3j.tx.gas.ContractGasProvider
import org.web3j.tx.gas.StaticEIP1559GasProvider
import java.math.BigDecimal
import java.math.BigInteger
import java.util.concurrent.TimeUnit
interface VisaContractInfoProvider {
suspend fun getContractInfo(walletAddress: String): VisaContractInfo
class Builder(
private val useTestnetRpc: Boolean,
private val bridgeProcessorAddress: String,
private val paymentAccountRegistryAddress: String,
private val isNetworkLoggingEnabled: Boolean,
private val dispatchers: CoroutineDispatcherProvider,
private val chainId: Long = Constants.CHAIN_ID,
private val decimals: Int = Constants.DECIMALS,
private val gasLimit: Long = Constants.GAS_LIMIT,
private val networkTimeoutSeconds: Long = Constants.NETWORK_TIMEOUT_SECONDS,
private val privateKey: String = ByteArray(Constants.PRIVATE_KEY_LENGTH).toHexString(),
) {
fun build(): VisaContractInfoProvider {
val web3j = createWeb3J()
val gasProvider = createGasProvider()
val transactionManager = createTransactionManager(web3j)
return DefaultVisaContractInfoProvider(
web3j = web3j,
transactionManager = transactionManager,
gasProvider = gasProvider,
bridgeProcessorAddress = bridgeProcessorAddress,
paymentAccountRegistryAddress = paymentAccountRegistryAddress,
dispatchers = dispatchers,
)
}
private fun createWeb3J(): Web3j {
val baseUrl: String = if (useTestnetRpc) Constants.TESTNET_RPC_URL else Constants.MAINNET_RPC_URL
val httpClient = OkHttpClient.Builder().apply {
connectTimeout(networkTimeoutSeconds, TimeUnit.SECONDS)
readTimeout(networkTimeoutSeconds, TimeUnit.SECONDS)
writeTimeout(networkTimeoutSeconds, TimeUnit.SECONDS)
if (isNetworkLoggingEnabled) {
addInterceptor(
LoggingInterceptor.Builder()
.setLevel(Level.BODY)
.tag(NETWORK_LOGS_TAG)
.build(),
)
}
}.build()
val web3jService = HttpService(
/* url = */ baseUrl,
/* httpClient = */ httpClient,
/* includeRawResponses = */ false,
)
return Web3j.build(web3jService)
}
private fun createGasProvider(): ContractGasProvider = StaticEIP1559GasProvider(
/* chainId = */ chainId,
/* maxFeePerGas = */ BigDecimal.ONE.movePointLeft(decimals).toBigInteger(),
/* maxPriorityFeePerGas = */ BigDecimal.ONE.movePointLeft(decimals).toBigInteger(),
/* gasLimit = */ BigInteger.valueOf(gasLimit),
)
private fun createTransactionManager(web3j: Web3j): TransactionManager = FastRawTransactionManager(
/* web3j = */ web3j,
/* credentials = */ Credentials.create(privateKey),
/* chainId = */ chainId,
)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.lib.visa.api
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.lib.visa.model.VisaTxHistoryResponse
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
interface VisaApi {
@GET("transaction")
suspend fun getTxHistory(
@Header("Authorization") authorizationHeader: String,
@Query("card_public_key") cardPublicKey: String,
@Query("limit") limit: Int,
@Query("offset") offset: Int,
): ApiResponse<VisaTxHistoryResponse>
}

View file

@ -0,0 +1,72 @@
package com.tangem.lib.visa.api
import android.util.Log
import com.ihsanbal.logging.Level
import com.ihsanbal.logging.LoggingInterceptor
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
import com.tangem.lib.visa.utils.Constants
import com.tangem.lib.visa.utils.Constants.NETWORK_LOGS_TAG
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.util.concurrent.TimeUnit
class VisaApiBuilder(
private val useDevApi: Boolean,
private val isNetworkLoggingEnabled: Boolean,
private val moshi: Moshi,
private val headers: Map<String, String>,
private val networkTimeoutSeconds: Long = Constants.NETWORK_TIMEOUT_SECONDS,
) {
fun build(): VisaApi {
val okHttpClient = createOkHttpClient()
val retrofit = createRetrofit(okHttpClient)
return retrofit.create(VisaApi::class.java)
}
private fun createOkHttpClient(): OkHttpClient {
val builder = OkHttpClient.Builder().apply {
connectTimeout(networkTimeoutSeconds, TimeUnit.SECONDS)
readTimeout(networkTimeoutSeconds, TimeUnit.SECONDS)
writeTimeout(networkTimeoutSeconds, TimeUnit.SECONDS)
if (isNetworkLoggingEnabled) {
addInterceptor(createNetworkLoggingInterceptor())
}
if (headers.isNotEmpty()) {
addInterceptor { chain ->
val request = chain.request().newBuilder().apply {
headers.forEach { (key, value) -> addHeader(key, value) }
}.build()
chain.proceed(request)
}
}
}
return builder.build()
}
private fun createRetrofit(okHttpClient: OkHttpClient): Retrofit {
val baseUrl = if (useDevApi) Constants.VISA_API_DEV_URL else Constants.VISA_API_PROD_URL
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
.baseUrl(baseUrl)
.client(okHttpClient)
.build()
}
}
private fun createNetworkLoggingInterceptor(): Interceptor {
return LoggingInterceptor.Builder()
.setLevel(Level.BODY)
.log(Log.VERBOSE)
.tag(NETWORK_LOGS_TAG)
.build()
}

View file

@ -0,0 +1,51 @@
package com.tangem.lib.visa.model
import org.joda.time.Instant
import java.math.BigDecimal
import java.math.BigInteger
data class VisaContractInfo(
val token: Token,
val balances: Balances,
val oldLimits: Limits,
val newLimits: Limits,
val limitsChangeDate: Instant,
) {
data class Token(
val name: String,
val symbol: String,
val decimals: Int,
val address: String,
)
data class Balances(
val total: BigDecimal,
val verified: BigDecimal,
val available: Available,
val blocked: BigDecimal,
val debt: BigDecimal,
val pendingRefund: BigDecimal,
) {
data class Available(
val forPayment: BigDecimal,
val forWithdrawal: BigDecimal,
val forDebtPayment: BigDecimal,
)
}
data class Limits(
val spendLimit: Limit,
val noOtpLimit: Limit,
val singleTransactionLimit: BigDecimal,
val expirationDate: Instant,
val spendPeriodSeconds: BigInteger,
) {
data class Limit(
val limit: BigDecimal,
val spent: BigDecimal,
)
}
}

View file

@ -0,0 +1,88 @@
package com.tangem.lib.visa.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.joda.time.DateTime
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class VisaTxHistoryResponse(
@Json(name = "card_wallet_address")
val cardWalletAddress: String,
@Json(name = "transactions")
val transactions: List<Transaction>,
) {
@JsonClass(generateAdapter = true)
data class Transaction(
@Json(name = "auth_code")
val authCode: String?,
@Json(name = "billing_amount")
val billingAmount: BigDecimal,
@Json(name = "billing_currency_code")
val billingCurrencyCode: Int,
@Json(name = "blockchain_amount")
val blockchainAmount: BigDecimal,
@Json(name = "blockchain_coin_name")
val blockchainCoinName: String,
@Json(name = "blockchain_fee")
val blockchainFee: BigDecimal,
// @Json(name = "local_dt")
// val localDate: DateTime?,
@Json(name = "merchant_category_code")
val merchantCategoryCode: String?,
@Json(name = "merchant_city")
val merchantCity: String?,
@Json(name = "merchant_country_code")
val merchantCountryCode: String?,
@Json(name = "merchant_name")
val merchantName: String?,
@Json(name = "requests")
val requests: List<Request>,
@Json(name = "rrn")
val rrn: String?,
@Json(name = "transaction_amount")
val transactionAmount: BigDecimal,
@Json(name = "transaction_currency_code")
val transactionCurrencyCode: Int,
@Json(name = "transaction_dt")
val transactionDt: DateTime,
@Json(name = "transaction_id")
val transactionId: Long,
@Json(name = "transaction_status")
val transactionStatus: String,
@Json(name = "transaction_type")
val transactionType: String,
) {
@JsonClass(generateAdapter = true)
data class Request(
@Json(name = "billing_amount")
val billingAmount: BigDecimal,
@Json(name = "billing_currency_code")
val billingCurrencyCode: Int,
@Json(name = "blockchain_amount")
val blockchainAmount: BigDecimal,
@Json(name = "blockchain_fee")
val blockchainFee: BigDecimal,
@Json(name = "error_code")
val errorCode: Int,
@Json(name = "request_dt")
val requestDt: DateTime,
@Json(name = "request_status")
val requestStatus: String,
@Json(name = "request_type")
val requestType: String,
@Json(name = "transaction_amount")
val transactionAmount: BigDecimal,
@Json(name = "transaction_currency_code")
val transactionCurrencyCode: Int,
@Json(name = "transaction_request_id")
val transactionRequestId: Long,
@Json(name = "tx_hash")
val txHash: String?,
@Json(name = "tx_status")
val txStatus: String?,
)
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.lib.visa.utils
import org.joda.time.Instant
import java.math.BigDecimal
import java.math.BigInteger
internal fun BigInteger.toBigDecimal(decimals: Int): BigDecimal {
return this.toBigDecimal().movePointLeft(decimals)
}
internal fun BigInteger.toInstant(): Instant {
return Instant.ofEpochSecond(toLong())
}

View file

@ -0,0 +1,3 @@
package com.tangem.lib.visa.utils
internal fun ByteArray.toHexString(): String = joinToString("") { "%02X".format(it) }

View file

@ -0,0 +1,18 @@
package com.tangem.lib.visa.utils
internal object Constants {
const val MAINNET_RPC_URL = "https://polygon-rpc.com/"
const val TESTNET_RPC_URL = "https://rpc-amoy.polygon.technology/"
const val CHAIN_ID = 80_001L
const val DECIMALS = 9
const val GAS_LIMIT = 500_000_000L
const val PRIVATE_KEY_LENGTH = 32
const val VISA_API_PROD_URL = "https://payapi.tangem-tech.com/api/v1/"
const val VISA_API_DEV_URL = "[REDACTED_ENV_URL]"
const val NETWORK_TIMEOUT_SECONDS = 65L
const val NETWORK_LOGS_TAG = "VisaNetworkLogs"
}