Updated on 2026-08-14

This commit is contained in:
Tangem 2024-04-17 12:32:52 +01:00
commit e8ccd67098
295 changed files with 4414 additions and 2971 deletions

View file

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

View file

@ -0,0 +1,19 @@
package com.tangem.datasource.api.tangemTech
import com.tangem.datasource.config.models.ProviderModel
import retrofit2.http.GET
import retrofit2.http.Header
/**
* Tangem Tech API for app services
*
[REDACTED_AUTHOR]
*/
interface TangemTechServiceApi {
@GET("v1/networks")
suspend fun getBlockchainProviders(
@Header("card_public_key") cardPublicKey: String,
@Header("card_id") cardId: String,
): Map<String, List<ProviderModel>>
}

View file

@ -0,0 +1,87 @@
package com.tangem.datasource.asset.loader
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.adapter
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import timber.log.Timber
import javax.inject.Inject
/**
* Asset file loader
*
[REDACTED_AUTHOR]
*/
class AssetLoader @Inject constructor(
val assetReader: AssetReader,
@NetworkMoshi val moshi: Moshi,
val dispatchers: CoroutineDispatcherProvider,
) {
/** Load content [Content] of asset file [fileName] */
@OptIn(ExperimentalStdlibApi::class)
suspend inline fun <reified Content> load(fileName: String): Content? {
return runCatching(dispatchers.io) {
val json = assetReader.readJson(fileName = fileName)
moshi.adapter<Content>().fromJson(json)
}
.fold(
onSuccess = { parsedConfig ->
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config is null"))
parsedConfig
},
onFailure = {
Timber.e(it, "Failed to load config from assets")
null
},
)
}
/** Load list [V] values of asset file [fileName] */
suspend inline fun <reified V> loadList(fileName: String): List<V> {
return runCatching(dispatchers.io) {
val json = assetReader.readJson(fileName = fileName)
val type = Types.newParameterizedType(List::class.java, V::class.java)
val adapter = moshi.adapter<List<V>>(type)
adapter.fromJson(json)
}
.fold(
onSuccess = { parsedConfig ->
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config is null"))
parsedConfig.orEmpty()
},
onFailure = {
Timber.e(it, "Failed to load config from assets")
emptyList()
},
)
}
/** Load map [String] keys and [V] values of asset file [fileName] */
suspend inline fun <reified V> loadMap(fileName: String): Map<String, V> {
return runCatching(dispatchers.io) {
val json = assetReader.readJson(fileName = fileName)
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
val adapter = moshi.adapter<Map<String, V>>(type)
adapter.fromJson(json)
}
.fold(
onSuccess = { parsedConfig ->
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config is null"))
parsedConfig.orEmpty()
},
onFailure = {
Timber.e(it, "Failed to load config from assets")
emptyMap()
},
)
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.asset
package com.tangem.datasource.asset.reader
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
@ -21,8 +21,7 @@ internal class AndroidAssetReader @Inject constructor(
.use(BufferedReader::readText)
}
override fun openFile(fileName: String): InputStream {
return context.assets
.open(fileName)
override fun openFile(file: String): InputStream {
return context.assets.open(file)
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.asset
package com.tangem.datasource.asset.reader
import java.io.InputStream

View file

@ -146,8 +146,8 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
blockBookRest = accessTokens.bitcoin?.blockBookRest,
),
algorand = GetBlockAccessToken(rest = accessTokens.algorand?.rest),
zkSync = GetBlockAccessToken(rest = accessTokens.zksync?.jsonRPC),
polygonZkevm = GetBlockAccessToken(rest = accessTokens.polygonZkevm?.jsonRPC),
zkSyncEra = GetBlockAccessToken(rest = accessTokens.zksync?.jsonRPC),
polygonZkEvm = GetBlockAccessToken(rest = accessTokens.polygonZkevm?.jsonRPC),
base = GetBlockAccessToken(rest = accessTokens.base?.jsonRPC),
)
}

View file

@ -2,7 +2,7 @@ package com.tangem.datasource.config
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.config.models.ConfigModel
import com.tangem.datasource.config.models.ConfigValueModel
import com.tangem.datasource.config.models.FeatureModel
@ -11,6 +11,7 @@ import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
@Deprecated(message = "Use AssetReader instead")
class FeaturesLocalLoader(
private val assetReader: AssetReader,
private val moshi: Moshi,

View file

@ -7,6 +7,7 @@ import com.squareup.moshi.JsonClass
[REDACTED_AUTHOR]
*/
// TODO remove
class FeatureModel(
val isTopUpEnabled: Boolean,
val isCreatingTwinCardsAllowed: Boolean,

View file

@ -0,0 +1,32 @@
package com.tangem.datasource.config.models
import com.squareup.moshi.Json
/** Config provider model */
sealed class ProviderModel {
/**
* Example,
* {
* "type": "public",
* "url": "https://example.com"
* }
*/
data class Public(
@Json(name = "url") val url: String,
) : ProviderModel()
/**
* Example,
* {
* "type": "private",
* "name": "nownodes"
* }
*/
data class Private(
@Json(name = "name") val name: String,
) : ProviderModel()
/** Unsupported type */
data object UnsupportedType : ProviderModel()
}

View file

@ -1,22 +0,0 @@
package com.tangem.datasource.di
import com.tangem.blockchain.common.AccountCreator
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.blockchain.DefaultAccountCreator
import com.tangem.lib.auth.AuthProvider
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 AccountCreatorModule {
@Provides
@Singleton
fun provideAccountCreator(authProvider: AuthProvider, tangemTechApi: TangemTechApi): AccountCreator {
return DefaultAccountCreator(authProvider, tangemTechApi)
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.datasource.di
import com.tangem.datasource.asset.AndroidAssetReader
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.asset.reader.AndroidAssetReader
import com.tangem.datasource.asset.reader.AssetReader
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn

View file

@ -1,21 +0,0 @@
package com.tangem.datasource.di
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.datasource.local.blockchain.DefaultBlockchainDataStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
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 BlockchainDataStorageModule {
@Provides
@Singleton
fun provideBlockchainDataStorage(appPreferencesStore: AppPreferencesStore): BlockchainDataStorage {
return DefaultBlockchainDataStorage(appPreferencesStore)
}
}

View file

@ -1,11 +1,13 @@
package com.tangem.datasource.di
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.common.BigDecimalAdapter
import com.tangem.datasource.api.common.DateTimeAdapter
import com.tangem.datasource.api.common.LocalDateAdapter
import com.tangem.datasource.config.models.ProviderModel
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -21,6 +23,12 @@ class MoshiModule {
@NetworkMoshi
fun provideNetworkMoshi(): Moshi {
return Moshi.Builder()
.add(
PolymorphicJsonAdapterFactory.of(ProviderModel::class.java, "type")
.withSubtype(ProviderModel.Public::class.java, "public")
.withSubtype(ProviderModel.Private::class.java, "private")
.withDefaultValue(ProviderModel.UnsupportedType),
)
.add(BigDecimalAdapter())
.add(LocalDateAdapter())
.add(DateTimeAdapter())

View file

@ -6,6 +6,7 @@ import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.TangemTechServiceApi
import com.tangem.datasource.utils.RequestHeader.*
import com.tangem.datasource.utils.addHeaders
import com.tangem.datasource.utils.addLoggers
@ -19,6 +20,7 @@ import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Module
@ -96,6 +98,33 @@ class NetworkModule {
.create(TangemTechApi::class.java)
}
@Provides
@Singleton
fun provideTangemTechServiceApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
appVersionProvider: AppVersionProvider,
): TangemTechServiceApi {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
.baseUrl(PROD_TANGEM_TECH_BASE_URL)
.client(
OkHttpClient.Builder()
.callTimeout(timeout = 5, unit = TimeUnit.SECONDS)
.addHeaders(
CacheControlHeader,
AppVersionPlatformHeaders(appVersionProvider),
// TODO("refactor header init") get auth data after biometric auth to avoid race condition
// AuthenticationHeader(authProvider),
)
.addLoggers(context)
.build(),
)
.build()
.create(TangemTechServiceApi::class.java)
}
private companion object {
const val PROD_EXPRESS_BASE_URL = "https://express.tangem.com/v1/"
const val DEV_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]"

View file

@ -2,7 +2,7 @@ package com.tangem.datasource.di
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.local.testnet.DefaultTestnetTokensStorage
import com.tangem.datasource.local.testnet.TestnetTokensStorage
import dagger.Module

View file

@ -1,31 +0,0 @@
package com.tangem.datasource.local.blockchain
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
import com.tangem.lib.auth.AuthProvider
internal class DefaultAccountCreator(
private val authProvider: AuthProvider,
private val tangemTechApi: TangemTechApi,
) : AccountCreator {
override suspend fun createAccount(blockchain: Blockchain, walletPublicKey: ByteArray): Result<String> {
val request = CreateUserNetworkAccountBody(blockchain.id.removeSuffix("/test"), walletPublicKey.toHexString())
return try {
val response = tangemTechApi.createUserNetworkAccount(
cardPublicKey = authProvider.getCardPublicKey(),
cardId = authProvider.getCardId(),
body = request,
).getOrThrow()
Result.Success(response.data.accountId)
} catch (e: Exception) {
Result.Failure(BlockchainSdkError.FailedToCreateAccount)
}
}
}

View file

@ -1,29 +0,0 @@
package com.tangem.datasource.local.blockchain
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
}
}
}

View file

@ -5,9 +5,13 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.APP_LAUNCH_COUNT_
import com.tangem.datasource.local.preferences.PreferencesKeys.FUNDS_FOUND_DATE_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TANGEM_TOS_ACCEPTED_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SAVE_USER_WALLETS_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_OPEN_WELCOME_ON_RESUME_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.USED_CARDS_INFO_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.WAS_APPLICATION_STOPPED_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN
/**
@ -19,6 +23,8 @@ object PreferencesKeys {
val SAVE_USER_WALLETS_KEY by lazy { booleanPreferencesKey(name = "saveUserWallets") }
val SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY by lazy { booleanPreferencesKey("saveUserWalletShown") }
val APP_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "launchCount") }
val SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "showRatingDialogAtLaunchCount") }
@ -75,6 +81,12 @@ object PreferencesKeys {
val SEND_TAP_HELP_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "sendTapHelpPreview") }
val WAS_APPLICATION_STOPPED_KEY by lazy { booleanPreferencesKey(name = "applicationStopped") }
val SHOULD_OPEN_WELCOME_ON_RESUME_KEY by lazy { booleanPreferencesKey(name = "openWelcomeOnResume") }
val SHOULD_SAVE_ACCESS_CODES_KEY by lazy { booleanPreferencesKey(name = "saveAccessCodes") }
fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region")
}
@ -82,6 +94,7 @@ object PreferencesKeys {
internal fun getTapPrefKeysToMigrate(): Set<String> {
return setOf(
SAVE_USER_WALLETS_KEY,
SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY,
APP_LAUNCH_COUNT_KEY,
SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY,
FUNDS_FOUND_DATE_KEY,
@ -89,6 +102,9 @@ internal fun getTapPrefKeysToMigrate(): Set<String> {
USED_CARDS_INFO_KEY,
WAS_TWINS_ONBOARDING_SHOWN,
IS_TANGEM_TOS_ACCEPTED_KEY,
WAS_APPLICATION_STOPPED_KEY,
SHOULD_OPEN_WELCOME_ON_RESUME_KEY,
SHOULD_SAVE_ACCESS_CODES_KEY,
)
.map(Preferences.Key<*>::name)
.toSet()

View file

@ -1,7 +1,7 @@
package com.tangem.datasource.local.testnet
import com.squareup.moshi.JsonAdapter
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.local.testnet.models.TestnetTokensConfig
/**
@ -17,6 +17,7 @@ internal class DefaultTestnetTokensStorage(
private val adapter: JsonAdapter<TestnetTokensConfig>,
) : TestnetTokensStorage {
@Deprecated(message = "Use AssetReader instead")
override fun getConfig(): TestnetTokensConfig {
return requireNotNull(
value = adapter.fromJson(

View file

@ -1,8 +1,7 @@
package com.tangem.datasource.utils
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.lib.auth.AppVersionProvider
import com.tangem.lib.auth.AuthBearerProvider
import com.tangem.lib.auth.AuthProvider
import com.tangem.lib.auth.ExpressAuthProvider
/**
@ -15,7 +14,7 @@ sealed class RequestHeader(vararg pairs: Pair<String, () -> String>) {
/** Header list */
val values: List<Pair<String, () -> String>> = pairs.toList()
object CacheControlHeader : RequestHeader("Cache-Control" to { "max-age=600" })
data object CacheControlHeader : RequestHeader("Cache-Control" to { "max-age=600" })
class AuthenticationHeader(authProvider: AuthProvider) : RequestHeader(
"card_id" to { authProvider.getCardId() },
@ -28,10 +27,6 @@ sealed class RequestHeader(vararg pairs: Pair<String, () -> String>) {
"session-id" to { expressAuthProvider.getSessionId() },
)
class AuthBearerHeader(authBearerProvider: AuthBearerProvider) : RequestHeader(
"Authorization" to { "Bearer " + authBearerProvider.getApiKey() },
)
class AppVersionPlatformHeaders(appVersionProvider: AppVersionProvider) : RequestHeader(
"version" to { appVersionProvider.getAppVersion() },
"platform" to { "android" },