Updated on 2026-08-14

This commit is contained in:
Tangem 2024-10-10 15:56:02 +04:00
commit 369ed25ac6
515 changed files with 6100 additions and 12756 deletions

View file

@ -34,12 +34,6 @@ internal class DefaultCardSdkConfigRepository(
}
}
override fun isBiometricsRequestPolicy(): Boolean {
return with(sdk.config.userCodeRequestPolicy) {
this is UserCodeRequestPolicy.AlwaysWithBiometrics && codeType == UserCodeType.AccessCode
}
}
override fun resetCardIdDisplayFormat() {
sdk.config.cardIdDisplayFormat = CardIdDisplayFormat.Full
}

View file

@ -3,7 +3,7 @@ package com.tangem.data.common.currency
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import timber.log.Timber
@ -39,14 +39,14 @@ class CryptoCurrencyFactory {
sdkToken: SdkToken,
blockchain: Blockchain,
extraDerivationPath: String?,
derivationStyleProvider: DerivationStyleProvider,
scanResponse: ScanResponse,
): CryptoCurrency.Token? {
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain")
return null
}
val network = getNetwork(blockchain, extraDerivationPath, derivationStyleProvider) ?: return null
val network = getNetwork(blockchain, extraDerivationPath, scanResponse) ?: return null
val id = getTokenId(network, sdkToken)
return CryptoCurrency.Token(
@ -64,24 +64,20 @@ class CryptoCurrencyFactory {
fun createCoin(
blockchain: Blockchain,
extraDerivationPath: String?,
derivationStyleProvider: DerivationStyleProvider,
scanResponse: ScanResponse,
): CryptoCurrency.Coin? {
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain")
return null
}
val network = getNetwork(blockchain, extraDerivationPath, derivationStyleProvider) ?: return null
val network = getNetwork(blockchain, extraDerivationPath, scanResponse) ?: return null
return createCoin(network)
}
fun createCoin(
networkId: String,
extraDerivationPath: String?,
derivationStyleProvider: DerivationStyleProvider,
): CryptoCurrency.Coin? {
fun createCoin(networkId: String, extraDerivationPath: String?, scanResponse: ScanResponse): CryptoCurrency.Coin? {
val blockchain = Blockchain.fromNetworkId(networkId) ?: Blockchain.Unknown
return createCoin(blockchain, extraDerivationPath, derivationStyleProvider)
return createCoin(blockchain, extraDerivationPath, scanResponse)
}
fun createCoin(network: Network): CryptoCurrency.Coin {
@ -102,7 +98,7 @@ class CryptoCurrencyFactory {
token: Token,
networkId: String,
extraDerivationPath: String?,
derivationStyleProvider: DerivationStyleProvider,
scanResponse: ScanResponse,
): CryptoCurrency.Token? {
val sdkToken = SdkToken(
name = token.name,
@ -116,7 +112,7 @@ class CryptoCurrencyFactory {
sdkToken = sdkToken,
blockchain = blockchain,
extraDerivationPath = extraDerivationPath,
derivationStyleProvider = derivationStyleProvider,
scanResponse = scanResponse,
)
}

View file

@ -4,6 +4,10 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.FeePaidCurrency
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.Network
import timber.log.Timber
@ -11,26 +15,11 @@ fun getBlockchain(networkId: Network.ID): Blockchain {
return Blockchain.fromId(networkId.value)
}
fun getNetwork(networkId: Network.ID, derivationPath: Network.DerivationPath): Network {
val blockchain = getBlockchain(networkId)
return Network(
id = networkId,
backendId = blockchain.toNetworkId(),
name = blockchain.getNetworkName(),
isTestnet = blockchain.isTestnet(),
derivationPath = derivationPath,
currencySymbol = blockchain.currency,
standardType = getNetworkStandardType(blockchain),
hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource,
canHandleTokens = blockchain.canHandleTokens(),
)
}
fun getNetwork(
blockchain: Blockchain,
extraDerivationPath: String?,
derivationStyleProvider: DerivationStyleProvider?,
canHandleTokens: Boolean,
): Network? {
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to convert Unknown blockchain to the domain network model")
@ -42,11 +31,44 @@ fun getNetwork(
backendId = blockchain.toNetworkId(),
name = blockchain.getNetworkName(),
isTestnet = blockchain.isTestnet(),
derivationPath = getNetworkDerivationPath(blockchain, extraDerivationPath, derivationStyleProvider),
derivationPath = getNetworkDerivationPath(
blockchain = blockchain,
extraDerivationPath = extraDerivationPath,
cardDerivationStyleProvider = derivationStyleProvider,
),
currencySymbol = blockchain.currency,
standardType = getNetworkStandardType(blockchain),
hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource,
canHandleTokens = blockchain.canHandleTokens(),
canHandleTokens = canHandleTokens,
)
}
fun getNetwork(networkId: Network.ID, derivationPath: Network.DerivationPath, scanResponse: ScanResponse): Network? {
val blockchain = getBlockchain(networkId)
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to convert Unknown blockchain to the domain network model")
return null
}
return Network(
id = networkId,
backendId = blockchain.toNetworkId(),
name = blockchain.getNetworkName(),
isTestnet = blockchain.isTestnet(),
derivationPath = derivationPath,
currencySymbol = blockchain.currency,
standardType = getNetworkStandardType(blockchain),
hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource,
canHandleTokens = scanResponse.card.canHandleToken(blockchain, scanResponse.cardTypesResolver),
)
}
fun getNetwork(blockchain: Blockchain, extraDerivationPath: String?, scanResponse: ScanResponse): Network? {
return getNetwork(
blockchain = blockchain,
extraDerivationPath = extraDerivationPath,
derivationStyleProvider = scanResponse.derivationStyleProvider,
canHandleTokens = scanResponse.card.canHandleToken(blockchain, scanResponse.cardTypesResolver),
)
}

View file

@ -6,9 +6,7 @@ import com.tangem.blockchainsdk.compatibility.l2BlockchainsList
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import timber.log.Timber
@ -42,17 +40,15 @@ class ResponseCryptoCurrenciesFactory {
return null
}
val cardDerivationStyleProvider = scanResponse.derivationStyleProvider
if (scanResponse.cardTypesResolver.isTestCard()) {
blockchain = blockchain.getTestnetVersion() ?: blockchain
}
val sdkToken = createSdkToken(responseToken)
return if (sdkToken == null) {
createCoin(blockchain, responseToken, cardDerivationStyleProvider)
createCoin(blockchain, responseToken, scanResponse)
} else {
createToken(blockchain, sdkToken, responseToken.derivationPath, cardDerivationStyleProvider)
createToken(blockchain, sdkToken, responseToken.derivationPath, scanResponse)
}
}
@ -71,9 +67,9 @@ class ResponseCryptoCurrenciesFactory {
private fun createCoin(
blockchain: Blockchain,
responseToken: UserTokensResponse.Token,
derivationStyleProvider: DerivationStyleProvider,
scanResponse: ScanResponse,
): CryptoCurrency.Coin? {
val network = getNetwork(blockchain, responseToken.derivationPath, derivationStyleProvider) ?: return null
val network = getNetwork(blockchain, responseToken.derivationPath, scanResponse) ?: return null
return CryptoCurrency.Coin(
id = getCoinId(network, blockchain.toCoinId()),
@ -117,9 +113,9 @@ class ResponseCryptoCurrenciesFactory {
blockchain: Blockchain,
sdkToken: Token,
responseDerivationPath: String?,
derivationStyleProvider: DerivationStyleProvider,
scanResponse: ScanResponse,
): CryptoCurrency.Token? {
val network = getNetwork(blockchain, responseDerivationPath, derivationStyleProvider)
val network = getNetwork(blockchain, responseDerivationPath, scanResponse)
?: return null
val id = getTokenId(network, sdkToken)

View file

@ -31,9 +31,10 @@ dependencies {
// region Core modules
implementation(projects.core.datasource)
implementation(projects.core.navigation)
implementation(projects.core.utils)
// endregion
implementation(projects.core.utils)
implementation(projects.domain.feedback)
implementation(projects.domain.legacy)

View file

@ -1,21 +1,18 @@
package com.tangem.data.feedback
import android.content.Context
import android.content.pm.PackageInfo
import android.os.Build
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.navigation.email.EmailSender
import com.tangem.data.feedback.converters.BlockchainInfoConverter
import com.tangem.data.feedback.converters.CardInfoConverter
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.domain.feedback.models.BlockchainInfo
import com.tangem.domain.feedback.models.PhoneInfo
import com.tangem.domain.feedback.models.UserWalletsInfo
import com.tangem.domain.feedback.models.*
import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.version.AppVersionProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import timber.log.Timber
@ -27,7 +24,8 @@ import java.io.File
* @property appLogsStore app logs store
* @property userWalletsListManager user wallets list manager
* @property walletManagersStore wallet managers store
* @property context context for getting app version
* @property emailSender email sender
* @property appVersionProvider app version provider
*
[REDACTED_AUTHOR]
*/
@ -35,7 +33,8 @@ internal class DefaultFeedbackRepository(
private val appLogsStore: AppLogsStore,
private val userWalletsListManager: UserWalletsListManager,
private val walletManagersStore: WalletManagersStore,
private val context: Context,
private val emailSender: EmailSender,
private val appVersionProvider: AppVersionProvider,
) : FeedbackRepository {
private val blockchainsErrors = MutableStateFlow<Map<UserWalletId, BlockchainErrorInfo>>(emptyMap())
@ -73,7 +72,7 @@ internal class DefaultFeedbackRepository(
return PhoneInfo(
phoneModel = Build.MODEL,
osVersion = Build.VERSION.SDK_INT.toString(),
appVersion = getAppVersion(),
appVersion = appVersionProvider.versionName,
)
}
@ -95,14 +94,14 @@ internal class DefaultFeedbackRepository(
override fun getLogFile(): File? = appLogsStore.getFile()
private fun getAppVersion(): String {
return runCatching { context.packageManager.getPackageInfo(context.packageName, 0) }
.fold(
onSuccess = PackageInfo::versionName,
onFailure = {
Timber.e(it)
"x.y.z"
},
)
override fun sendEmail(feedbackEmail: FeedbackEmail) {
emailSender.send(
EmailSender.Email(
address = feedbackEmail.address,
subject = feedbackEmail.subject,
message = feedbackEmail.message,
attachment = feedbackEmail.file,
),
)
}
}

View file

@ -1,15 +1,15 @@
package com.tangem.data.feedback.di
import android.content.Context
import com.tangem.core.navigation.email.EmailSender
import com.tangem.data.feedback.DefaultFeedbackRepository
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.utils.version.AppVersionProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@ -23,13 +23,15 @@ internal object FeedbackRepositoryModule {
appLogsStore: AppLogsStore,
userWalletsListManager: UserWalletsListManager,
walletManagersStore: WalletManagersStore,
@ApplicationContext context: Context,
emailSender: EmailSender,
appVersionProvider: AppVersionProvider,
): FeedbackRepository {
return DefaultFeedbackRepository(
appLogsStore = appLogsStore,
userWalletsListManager = userWalletsListManager,
walletManagersStore = walletManagersStore,
context = context,
emailSender = emailSender,
appVersionProvider = appVersionProvider,
)
}
}

View file

@ -18,10 +18,8 @@ import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.extensions.canHandleBlockchain
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.extensions.supportedBlockchains
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.managetokens.model.AddCustomTokenForm
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.managetokens.repository.CustomTokensRepository
@ -65,9 +63,12 @@ internal class DefaultCustomTokensRepository(
contractAddress: String?,
): Boolean {
return withContext(dispatchers.io) {
val storedCurrencies: UserTokensResponse = appPreferencesStore.getObjectSyncOrNull(
val storedCurrencies = appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue),
) ?: error("User tokens not found")
)
requireNotNull(storedCurrencies) {
"User tokens not found for user wallet [$userWalletId] while checking if currency is not added"
}
storedCurrencies.tokens.none { token ->
Blockchain.fromId(networkId.value).toNetworkId() == token.networkId &&
@ -83,9 +84,12 @@ internal class DefaultCustomTokensRepository(
networkId: Network.ID,
derivationPath: Network.DerivationPath,
): CryptoCurrency.Token? = withContext(dispatchers.io) {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId)
?: error("User wallet not found")
val network = getNetwork(networkId, derivationPath)
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"User wallet [$userWalletId] not found while finding token"
}
val network = requireNotNull(getNetwork(networkId, derivationPath, userWallet.scanResponse)) {
"Network [$networkId] not found while finding token"
}
val tokenAddress = tokenAddressConverter.convertTokenAddress(
networkId,
contractAddress,
@ -125,13 +129,22 @@ internal class DefaultCustomTokensRepository(
}
}
override fun createCoin(networkId: Network.ID, derivationPath: Network.DerivationPath): CryptoCurrency.Coin {
val network = getNetwork(networkId, derivationPath)
override suspend fun createCoin(
userWalletId: UserWalletId,
networkId: Network.ID,
derivationPath: Network.DerivationPath,
): CryptoCurrency.Coin {
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"User wallet [$userWalletId] not found while creating coin"
}
val network = requireNotNull(getNetwork(networkId, derivationPath, userWallet.scanResponse)) {
"Network [$networkId] not found while creating coin"
}
return cryptoCurrencyFactory.createCoin(network)
}
override fun createToken(
override suspend fun createToken(
managedCryptoCurrency: ManagedCryptoCurrency.Token,
sourceNetwork: ManagedCryptoCurrency.SourceNetwork.Default,
rawId: String?,
@ -147,11 +160,17 @@ internal class DefaultCustomTokensRepository(
}
override suspend fun createCustomToken(
userWalletId: UserWalletId,
networkId: Network.ID,
derivationPath: Network.DerivationPath,
formValues: AddCustomTokenForm.Validated.All,
): CryptoCurrency.Token {
val network = getNetwork(networkId, derivationPath)
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"User wallet [$userWalletId] not found while creating custom token"
}
val network = requireNotNull(getNetwork(networkId, derivationPath, userWallet.scanResponse)) {
"Network [$networkId] not found while creating custom token"
}
val tokenAddress = tokenAddressConverter.convertTokenAddress(
networkId,
formValues.contractAddress,
@ -171,7 +190,11 @@ internal class DefaultCustomTokensRepository(
override suspend fun removeCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) =
withContext(dispatchers.io) {
val cryptoCurrency = when (currency) {
is ManagedCryptoCurrency.Custom.Coin -> createCoin(currency.network.id, currency.network.derivationPath)
is ManagedCryptoCurrency.Custom.Coin -> createCoin(
userWalletId = userWalletId,
networkId = currency.network.id,
derivationPath = currency.network.derivationPath,
)
is ManagedCryptoCurrency.Custom.Token -> cryptoCurrencyFactory.createToken(
network = currency.network,
rawId = currency.currencyId.rawCurrencyId,
@ -181,15 +204,17 @@ internal class DefaultCustomTokensRepository(
contractAddress = currency.contractAddress,
)
}
val savedCurrencies = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform remove currency action" },
val storedCurrencies = appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue),
)
requireNotNull(storedCurrencies) {
"User tokens not found for user wallet [$userWalletId] while removing currency"
}
val token = userTokensResponseFactory.createResponseToken(cryptoCurrency)
storeAndPushTokens(
userWalletId = userWalletId,
response = savedCurrencies.copy(tokens = savedCurrencies.tokens.filterNot { it == token }),
response = storedCurrencies.copy(tokens = storedCurrencies.tokens.filterNot { it == token }),
)
when (cryptoCurrency) {
is CryptoCurrency.Coin -> walletManagersFacade.remove(userWalletId, setOf(cryptoCurrency.network))
@ -198,8 +223,9 @@ internal class DefaultCustomTokensRepository(
}
override suspend fun getSupportedNetworks(userWalletId: UserWalletId): List<Network> = withContext(dispatchers.io) {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId)
?: error("User wallet not found")
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"User wallet [$userWalletId] not found while getting supported networks"
}
val scanResponse = userWallet.scanResponse
Blockchain.entries
@ -208,9 +234,7 @@ internal class DefaultCustomTokensRepository(
getNetwork(
blockchain = blockchain,
extraDerivationPath = null,
derivationStyleProvider = scanResponse.derivationStyleProvider,
)?.copy(
canHandleTokens = scanResponse.card.canHandleToken(blockchain, scanResponse.cardTypesResolver),
scanResponse = scanResponse,
)
} else {
null
@ -238,13 +262,7 @@ internal class DefaultCustomTokensRepository(
private suspend fun pushTokens(userWalletId: UserWalletId, response: UserTokensResponse) {
safeApiCall({ tangemTechApi.saveUserTokens(userWalletId.stringValue, response).bind() }) {
Timber.e(it, "Unable to save user tokens for: ${userWalletId.stringValue}")
Timber.e(it, "Unable to push user tokens for: ${userWalletId.stringValue}")
}
}
private suspend fun getSavedUserTokensResponseSync(key: UserWalletId): UserTokensResponse? {
return appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(key.stringValue),
)
}
}

View file

@ -12,10 +12,10 @@ import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.config.testnet.TestnetTokensStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.testnet.TestnetTokensStorage
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.extensions.canHandleBlockchain
@ -23,7 +23,6 @@ import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.extensions.supportedBlockchains
import com.tangem.domain.common.extensions.supportedTokens
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.managetokens.model.*
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
import com.tangem.domain.managetokens.repository.ManageTokensRepository
@ -119,13 +118,13 @@ internal class DefaultManageTokensRepository(
managedCryptoCurrencyFactory.createWithCustomTokens(
coinsResponse = updatedCoinsResponse,
tokensResponse = tokensResponse,
derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider,
scanResponse = userWallet.scanResponse,
)
} else {
managedCryptoCurrencyFactory.create(
coinsResponse = updatedCoinsResponse,
tokensResponse = tokensResponse,
derivationStyleProvider = userWallet?.scanResponse?.derivationStyleProvider,
scanResponse = userWallet?.scanResponse,
)
}
@ -155,7 +154,7 @@ internal class DefaultManageTokensRepository(
testnetTokensConfig
},
tokensResponse = getSavedUserTokensResponseSync(userWallet.walletId),
derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider,
scanResponse = userWallet.scanResponse,
)
return BatchFetchResult.Success(

View file

@ -4,8 +4,8 @@ import com.tangem.data.managetokens.DefaultCustomTokensRepository
import com.tangem.data.managetokens.DefaultManageTokensRepository
import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.config.testnet.TestnetTokensStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.testnet.TestnetTokensStorage
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.managetokens.repository.CustomTokensRepository
import com.tangem.domain.managetokens.repository.ManageTokensRepository

View file

@ -12,10 +12,14 @@ import com.tangem.data.common.currency.getNetwork
import com.tangem.data.common.currency.getTokenId
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.testnet.models.TestnetTokensConfig
import com.tangem.datasource.local.config.testnet.models.TestnetTokensConfig
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.Network
internal class ManagedCryptoCurrencyFactory {
@ -23,20 +27,20 @@ internal class ManagedCryptoCurrencyFactory {
fun create(
coinsResponse: CoinsResponse,
tokensResponse: UserTokensResponse?,
derivationStyleProvider: DerivationStyleProvider?,
scanResponse: ScanResponse?,
): List<ManagedCryptoCurrency> {
return coinsResponse.coins.mapNotNull { coin ->
createToken(coin, tokensResponse, coinsResponse.imageHost, derivationStyleProvider)
createToken(coin, tokensResponse, coinsResponse.imageHost, scanResponse)
}
}
fun createWithCustomTokens(
coinsResponse: CoinsResponse,
tokensResponse: UserTokensResponse,
derivationStyleProvider: DerivationStyleProvider,
scanResponse: ScanResponse,
): List<ManagedCryptoCurrency> {
val customTokens = createCustomTokens(tokensResponse, derivationStyleProvider)
val tokens = create(coinsResponse, tokensResponse, derivationStyleProvider)
val customTokens = createCustomTokens(tokensResponse, scanResponse)
val tokens = create(coinsResponse, tokensResponse, scanResponse)
return customTokens + tokens
}
@ -44,10 +48,10 @@ internal class ManagedCryptoCurrencyFactory {
fun createTestnetWithCustomTokens(
testnetTokensConfig: TestnetTokensConfig,
tokensResponse: UserTokensResponse?,
derivationStyleProvider: DerivationStyleProvider,
scanResponse: ScanResponse,
): List<ManagedCryptoCurrency> {
val customTokens = tokensResponse
?.let { createCustomTokens(it, derivationStyleProvider) }
?.let { createCustomTokens(it, scanResponse) }
?: emptyList()
val testnetTokens = testnetTokensConfig.tokens.map { testnetToken ->
ManagedCryptoCurrency.Token(
@ -60,10 +64,10 @@ internal class ManagedCryptoCurrencyFactory {
networkId = network.id,
contractAddress = network.address,
decimals = network.decimalCount ?: return@mapNotNull null,
derivationStyleProvider = derivationStyleProvider,
scanResponse = scanResponse,
)
} ?: emptyList(),
addedIn = findAddedInNetworks(testnetToken.id, tokensResponse, derivationStyleProvider),
addedIn = findAddedInNetworks(testnetToken.id, tokensResponse, scanResponse),
)
}
@ -72,29 +76,29 @@ internal class ManagedCryptoCurrencyFactory {
private fun createCustomTokens(
tokensResponse: UserTokensResponse,
derivationStyleProvider: DerivationStyleProvider,
scanResponse: ScanResponse,
): List<ManagedCryptoCurrency> = tokensResponse.tokens
.mapNotNull { token ->
maybeCreateCustomToken(token, null, derivationStyleProvider)
maybeCreateCustomToken(token, null, scanResponse)
}
private fun maybeCreateCustomToken(
token: UserTokensResponse.Token,
imageHost: String?,
derivationStyleProvider: DerivationStyleProvider,
scanResponse: ScanResponse,
): ManagedCryptoCurrency? {
val blockchain = Blockchain.fromNetworkId(token.networkId)
?.takeIf { it.isSupportedInApp() }
?: return null
if (!checkIsCustomToken(token, blockchain, derivationStyleProvider)) {
if (!checkIsCustomToken(token, blockchain, scanResponse.derivationStyleProvider)) {
return null
}
val network = getNetwork(
blockchain = blockchain,
extraDerivationPath = token.derivationPath,
derivationStyleProvider = derivationStyleProvider,
scanResponse = scanResponse,
) ?: return null
val contractAddress = token.contractAddress
@ -123,7 +127,7 @@ internal class ManagedCryptoCurrencyFactory {
coinResponse: CoinsResponse.Coin,
tokensResponse: UserTokensResponse?,
imageHost: String?,
derivationStyleProvider: DerivationStyleProvider?,
scanResponse: ScanResponse?,
): ManagedCryptoCurrency? {
if (coinResponse.networks.isEmpty() || !coinResponse.active) return null
@ -134,7 +138,7 @@ internal class ManagedCryptoCurrencyFactory {
networkId = network.networkId,
contractAddress = network.contractAddress,
decimals = network.decimalCount?.toInt(),
derivationStyleProvider = derivationStyleProvider,
scanResponse = scanResponse,
)
}
.ifEmpty { return null }
@ -145,7 +149,7 @@ internal class ManagedCryptoCurrencyFactory {
symbol = coinResponse.symbol,
iconUrl = getIconUrl(coinResponse.id, imageHost),
availableNetworks = availableNetworks,
addedIn = findAddedInNetworks(coinResponse.id, tokensResponse, derivationStyleProvider),
addedIn = findAddedInNetworks(coinResponse.id, tokensResponse, scanResponse),
)
}
@ -153,14 +157,21 @@ internal class ManagedCryptoCurrencyFactory {
networkId: String,
contractAddress: String?,
decimals: Int?,
derivationStyleProvider: DerivationStyleProvider?,
scanResponse: ScanResponse?,
extraDerivationPath: String? = null,
): SourceNetwork? {
val blockchain = Blockchain.fromNetworkId(networkId)
?.takeIf { it.isSupportedInApp() }
?: return null
val network = getNetwork(blockchain, extraDerivationPath, derivationStyleProvider) ?: return null
val network = getNetwork(
blockchain,
extraDerivationPath,
scanResponse?.derivationStyleProvider,
canHandleTokens = scanResponse?.let {
it.card.canHandleToken(blockchain, it.cardTypesResolver)
} ?: true,
) ?: return null
return when {
contractAddress.isNullOrBlank() -> SourceNetwork.Main(
@ -180,7 +191,7 @@ internal class ManagedCryptoCurrencyFactory {
private fun findAddedInNetworks(
currencyId: String,
tokensResponse: UserTokensResponse?,
derivationStyleProvider: DerivationStyleProvider?,
scanResponse: ScanResponse?,
): Set<Network> {
if (tokensResponse == null) return emptySet()
@ -191,9 +202,12 @@ internal class ManagedCryptoCurrencyFactory {
if (blockchain != null && blockchain.isSupportedInApp()) {
getNetwork(
blockchain = blockchain,
extraDerivationPath = token.derivationPath,
derivationStyleProvider = derivationStyleProvider,
blockchain,
token.derivationPath,
scanResponse?.derivationStyleProvider,
canHandleTokens = scanResponse?.let {
it.card.canHandleToken(blockchain, it.cardTypesResolver)
} ?: true,
)
} else {
null

View file

@ -16,7 +16,6 @@ import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.TangemTechApi.Companion.marketsQuoteFields
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.markets.*
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.tokens.model.CryptoCurrency
@ -245,13 +244,13 @@ internal class DefaultMarketsTokenRepository(
CryptoCurrencyFactory().createCoin(
blockchain = blockchain,
extraDerivationPath = null,
derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider,
scanResponse = userWallet.scanResponse,
)
} else {
val currencyNetwork = getNetwork(
blockchain = blockchain,
extraDerivationPath = null,
derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider,
scanResponse = userWallet.scanResponse,
) ?: return null
CryptoCurrencyFactory().createToken(
@ -265,6 +264,14 @@ internal class DefaultMarketsTokenRepository(
}
}
override suspend fun getTokenExchanges(tokenId: String): List<TokenMarketExchange> {
return withContext(dispatcherProvider.io) {
val response = marketsApi.getCoinExchanges(coinId = tokenId).getOrThrow()
TokenMarketExchangeConverter.convertList(input = response.exchanges)
}
}
private inline fun <T> catchApiErrorAndSendEvent(errorEvent: MarketsDataAnalyticsEvent, block: () -> T): T {
return try {
block()

View file

@ -0,0 +1,35 @@
package com.tangem.data.markets.converters
import com.tangem.datasource.api.markets.models.response.TokenMarketExchangesResponse
import com.tangem.domain.markets.TokenMarketExchange
import com.tangem.utils.converter.Converter
/**
* Converter from [TokenMarketExchangesResponse.Exchange] to [TokenMarketExchange]
*
[REDACTED_AUTHOR]
*/
internal object TokenMarketExchangeConverter : Converter<TokenMarketExchangesResponse.Exchange, TokenMarketExchange> {
private val RISKY_INTERVAL = 0..3
private val CAUTION_INTERVAL = 4..7
override fun convert(value: TokenMarketExchangesResponse.Exchange): TokenMarketExchange {
return with(value) {
TokenMarketExchange(
id = id,
name = name,
imageUrl = imageUrl,
isCentralized = isCentralized,
volumeInUsd = volumeInUsd,
trustScore = when (trustScore) {
null,
in RISKY_INTERVAL,
-> TokenMarketExchange.TrustScore.Risky
in CAUTION_INTERVAL -> TokenMarketExchange.TrustScore.Caution
else -> TokenMarketExchange.TrustScore.Trusted
},
)
}
}
}

View file

@ -25,6 +25,7 @@ internal object TokenMarketInfoConverter : Converter<TokenMarketInfoResponse, To
metrics = metrics?.convert(),
links = links?.convert(),
pricePerformance = pricePerformance?.convert(),
exchangesAmount = exchangesAmount,
)
}
}

1
data/onramp/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,50 @@
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.data.onramp"
}
dependencies {
/** Core modules */
implementation(projects.core.datasource)
implementation(projects.core.utils)
/** Common modules */
implementation(projects.data.common)
/** Domain modules */
implementation(projects.domain.onramp)
// region DI
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
// endregion
// region Others dependencies
implementation(deps.androidx.datastore)
implementation(deps.jodatime)
implementation(deps.kotlin.coroutines)
implementation(deps.kotlin.immutable.collections)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
implementation(projects.libs.blockchainSdk)
implementation(projects.libs.crypto)
implementation(deps.tangem.card.core)
implementation(deps.tangem.blockchain) {
exclude(module = "joda-time")
}
// endregion
}

View file

@ -0,0 +1,19 @@
package com.tangem.data.onramp
import com.tangem.data.onramp.converters.error.OnrampErrorConverter
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
internal class DefaultOnrampErrorResolver(
private val onrampErrorConverter: OnrampErrorConverter,
) : OnrampErrorResolver {
override fun resolve(throwable: Throwable): OnrampError {
return if (throwable is ApiResponseError.HttpException) {
onrampErrorConverter.convert(throwable.errorBody.orEmpty())
} else {
OnrampError.UnknownError
}
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.data.onramp
import com.tangem.data.onramp.converters.CountryConverter
import com.tangem.data.onramp.converters.CurrencyConverter
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
internal class DefaultOnrampRepository(
private val onrampApi: OnrampApi,
private val dispatchers: CoroutineDispatcherProvider,
) : OnrampRepository {
private val currencyConverter = CurrencyConverter()
private val countryConverter = CountryConverter()
override suspend fun getCurrencies(): List<OnrampCurrency> = withContext(dispatchers.io) {
onrampApi.getCurrencies()
.getOrThrow()
.map(currencyConverter::convert)
}
override suspend fun getCountries(): List<OnrampCountry> = withContext(dispatchers.io) {
onrampApi.getCountries()
.getOrThrow()
.map(countryConverter::convert)
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.data.onramp.converters
import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.utils.converter.Converter
internal class CountryConverter : Converter<OnrampCountryDTO, OnrampCountry> {
private val currencyConverter = CurrencyConverter()
override fun convert(value: OnrampCountryDTO) = OnrampCountry(
name = value.name,
code = value.code,
image = value.image,
alpha3 = value.alpha3,
continent = value.continent,
defaultCurrency = currencyConverter.convert(value.defaultCurrency),
onrampAvailable = value.onrampAvailable,
)
}

View file

@ -0,0 +1,15 @@
package com.tangem.data.onramp.converters
import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.utils.converter.Converter
internal class CurrencyConverter : Converter<OnrampCurrencyDTO, OnrampCurrency> {
override fun convert(value: OnrampCurrencyDTO) = OnrampCurrency(
name = value.name,
code = value.code,
image = value.image,
precision = value.precision,
)
}

View file

@ -0,0 +1,11 @@
package com.tangem.data.onramp.converters.error
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.utils.converter.Converter
internal class OnrampErrorConverter : Converter<String, OnrampError> {
override fun convert(value: String): OnrampError {
return OnrampError.UnknownError
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.data.onramp.di
import com.tangem.data.onramp.DefaultOnrampRepository
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
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 OnrampDataModule {
@Provides
@Singleton
fun provideOnrampRepository(onrampApi: OnrampApi, dispatchers: CoroutineDispatcherProvider): OnrampRepository {
return DefaultOnrampRepository(
onrampApi = onrampApi,
dispatchers = dispatchers,
)
}
}

View file

@ -31,5 +31,6 @@ dependencies {
implementation(deps.kotlin.coroutines)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
// endregion
}

View file

@ -1,17 +1,31 @@
package com.tangem.data.settings
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.GeoResponse
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.Locale
internal class DefaultSettingsRepository(
private val appPreferencesStore: AppPreferencesStore,
private val appLogsStore: AppLogsStore,
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
) : SettingsRepository {
private val userCountryFlow = MutableStateFlow<UserCountry?>(value = null)
override suspend fun shouldShowSaveUserWalletScreen(): Boolean {
return appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY,
@ -99,4 +113,38 @@ internal class DefaultSettingsRepository(
override suspend fun setMarketsTooltipShown(value: Boolean) {
appPreferencesStore.store(key = PreferencesKeys.SHOULD_SHOW_MARKETS_TOOLTIP_KEY, value = !value)
}
override suspend fun getUserCountryCodeSync(): UserCountry? {
// If user country code is already set, return it
val countryCode = userCountryFlow.value
if (countryCode != null) return countryCode
coroutineScope {
launch { fetchUserCountryCode() }
}
return null
}
override suspend fun fetchUserCountryCode() {
Timber.i("Start fetching user country code")
withContext(dispatchers.io) {
val country = runCatching { tangemTechApi.getUserCountryCode() }
.fold(
onSuccess = GeoResponse::code,
onFailure = { Locale.getDefault().country },
)
.lowercase()
val code = when (country) {
UserCountry.Russia.code -> UserCountry.Russia
else -> UserCountry.Other(code = country)
}
Timber.i("User code country is $code")
userCountryFlow.value = code
}
}
}

View file

@ -4,12 +4,14 @@ import com.tangem.data.settings.DefaultAppRatingRepository
import com.tangem.data.settings.DefaultPermissionRepository
import com.tangem.data.settings.DefaultPromoSettingsRepository
import com.tangem.data.settings.DefaultSettingsRepository
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.settings.repositories.AppRatingRepository
import com.tangem.domain.settings.repositories.PermissionRepository
import com.tangem.domain.settings.repositories.PromoSettingsRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -25,8 +27,15 @@ internal object SettingsDataModule {
fun provideSettingsRepository(
appPreferencesStore: AppPreferencesStore,
appLogsStore: AppLogsStore,
tangemTechApi: TangemTechApi,
dispatchers: CoroutineDispatcherProvider,
): SettingsRepository {
return DefaultSettingsRepository(appPreferencesStore = appPreferencesStore, appLogsStore = appLogsStore)
return DefaultSettingsRepository(
appPreferencesStore = appPreferencesStore,
appLogsStore = appLogsStore,
tangemTechApi = tangemTechApi,
dispatchers = dispatchers,
)
}
@Provides

View file

@ -4,12 +4,14 @@ import android.util.Base64
import arrow.core.getOrElse
import arrow.core.raise.catch
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.blockchainsdk.utils.toMigratedCointId
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toCompressedPublicKey
import com.tangem.data.common.cache.CacheRegistry
@ -56,6 +58,7 @@ import com.tangem.utils.extensions.orZero
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
internal class DefaultStakingRepository(
@ -110,8 +113,8 @@ internal class DefaultStakingRepository(
rawNetworkId.plus(rawCurrencyId)
}
override fun isStakingSupported(integrationKey: String): Boolean {
return integrationIdMap.containsKey(integrationKey)
override fun getSupportedIntegrationId(cryptoCurrencyId: CryptoCurrency.ID): String? {
return integrationIdMap.getOrDefault(getIntegrationKey(cryptoCurrencyId), null)
}
override suspend fun fetchEnabledYields(refresh: Boolean) {
@ -120,10 +123,10 @@ internal class DefaultStakingRepository(
key = YIELDS_STORE_KEY,
skipCache = refresh,
block = {
val stakingTokensWithYields = stakeKitApi.getMultipleYields(preferredValidatorsOnly = false)
val stakingTokensWithYields = stakeKitApi.getEnabledYields(preferredValidatorsOnly = false)
.getOrThrow()
stakingYieldsStore.store(stakingTokensWithYields.data)
stakingYieldsStore.store(stakingTokensWithYields.data.filter { it.isAvailable })
},
)
}
@ -131,11 +134,10 @@ internal class DefaultStakingRepository(
override suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield {
return withContext(dispatchers.io) {
val yields = getEnabledYields() ?: error("No yields found")
val rawCurrencyId = cryptoCurrencyId.rawCurrencyId ?: error("Staking custom tokens is not available")
val prefetchedYield = findPrefetchedYield(
yields = yields,
yields = getEnabledYields(),
currencyId = rawCurrencyId,
symbol = symbol,
)
@ -156,7 +158,7 @@ internal class DefaultStakingRepository(
}
}
override suspend fun getStakingAvailability(
override fun getStakingAvailability(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): StakingAvailability {
@ -164,21 +166,22 @@ internal class DefaultStakingRepository(
val rawCurrencyId = cryptoCurrency.id.rawCurrencyId ?: return StakingAvailability.Unavailable
return withContext(dispatchers.io) {
val yields = getEnabledYields() ?: return@withContext StakingAvailability.Unavailable
val isSupportedInMobileApp = getSupportedIntegrationId(cryptoCurrency.id).isNullOrEmpty().not()
val prefetchedYield = findPrefetchedYield(yields, rawCurrencyId, cryptoCurrency.symbol)
val isSupported = isStakingSupported(getIntegrationKey(cryptoCurrency.id))
val prefetchedYield = findPrefetchedYield(
yields = getEnabledYields(),
currencyId = rawCurrencyId,
symbol = cryptoCurrency.symbol,
)
when {
prefetchedYield != null && isSupported -> {
StakingAvailability.Available(prefetchedYield.id)
}
prefetchedYield == null && isSupported -> {
StakingAvailability.TemporaryDisabled
}
else -> StakingAvailability.Unavailable
return when {
prefetchedYield != null && isSupportedInMobileApp -> {
StakingAvailability.Available(prefetchedYield.id)
}
prefetchedYield == null && isSupportedInMobileApp -> {
StakingAvailability.TemporaryUnavailable
}
else -> StakingAvailability.Unavailable
}
}
@ -264,6 +267,7 @@ internal class DefaultStakingRepository(
override suspend fun constructTransaction(
networkId: String,
fee: Fee,
amount: Amount,
transactionId: String,
): Pair<StakingTransaction, TransactionData.Compiled> {
return withContext(dispatchers.io) {
@ -277,6 +281,7 @@ internal class DefaultStakingRepository(
val transactionData = TransactionData.Compiled(
value = getTransactionDataType(networkId, unsignedTransaction),
fee = fee,
amount = amount,
status = TransactionStatus.Unconfirmed,
)
@ -300,7 +305,10 @@ internal class DefaultStakingRepository(
if (integrationId == null || address.isNullOrBlank()) {
cacheRegistry.invalidate(getYieldBalancesKey(userWalletId))
error("IntegrationId or address is null")
Timber.w(
"IntegrationId or address is null fetching ${cryptoCurrency.name} staking balance",
)
return@invokeOnExpire
}
val requestBody = getBalanceRequestData(address, integrationId)
@ -335,8 +343,13 @@ internal class DefaultStakingRepository(
val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)]
?: error("Could not get integrationId")
stakingBalanceStore.get(userWalletId, address, integrationId)
.distinctUntilChanged()
.collectLatest {
send(yieldBalanceConverter.convert(it))
if (it != null) {
send(yieldBalanceConverter.convert(it))
} else {
error("No yield balance available for currency ${cryptoCurrency.id.value}")
}
}
}
@ -384,12 +397,13 @@ internal class DefaultStakingRepository(
key = getYieldBalancesKey(userWalletId),
skipCache = refresh,
block = {
val yields = getEnabledYields()
val availableCurrencies = cryptoCurrencies
.mapNotNull { currency ->
val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network)
val integrationId = integrationIdMap[getIntegrationKey(currency.id)]
if (integrationId != null) {
if (integrationId != null && yields.any { it.id == integrationId }) {
addresses to integrationId
} else {
null
@ -537,9 +551,9 @@ internal class DefaultStakingRepository(
override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval {
return when (getIntegrationKey(cryptoCurrency.id)) {
Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() -> {
StakingApproval.Needed(ETHEREUM_POLYGON_APPROVE_SPENDER)
}
Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId(),
Blockchain.Ethereum.id + Blockchain.Polygon.toMigratedCointId(),
-> StakingApproval.Needed(ETHEREUM_POLYGON_APPROVE_SPENDER)
else -> StakingApproval.Empty
}
}
@ -563,12 +577,15 @@ internal class DefaultStakingRepository(
}
private fun findPrefetchedYield(yields: List<Yield>, currencyId: String, symbol: String): Yield? {
return yields.find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol }
return yields.find { yield ->
yield.tokens.any { it.coinGeckoId == currencyId && it.symbol == symbol }
}
}
private suspend fun getEnabledYields(): List<Yield>? {
val yields = stakingYieldsStore.getSyncOrNull() ?: return null
return yields.map { yieldConverter.convert(it) }
private fun getEnabledYields(): List<Yield> {
return stakingYieldsStore
.get()
.map { yieldConverter.convert(it) }
}
private fun getBalanceRequestData(address: String, integrationId: String): YieldBalanceRequestBody {
@ -621,8 +638,9 @@ internal class DefaultStakingRepository(
Blockchain.Solana.run { id + toCoinId() } to SOLANA_INTEGRATION_ID,
Blockchain.Cosmos.run { id + toCoinId() } to COSMOS_INTEGRATION_ID,
Blockchain.Tron.run { id + toCoinId() } to TRON_INTEGRATION_ID,
// Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID,
// Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID,
Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID,
Blockchain.Ethereum.id + Blockchain.Polygon.toMigratedCointId() to ETHEREUM_POLYGON_INTEGRATION_ID,
Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID,
// Blockchain.Polkadot.run { id + toCoinId() } to POLKADOT_INTEGRATION_ID,
// Blockchain.Avalanche.run { id + toCoinId() } to AVALANCHE_INTEGRATION_ID,
// Blockchain.Cronos.run { id + toCoinId() } to CRONOS_INTEGRATION_ID,

View file

@ -10,6 +10,7 @@ internal object PendingTransactionItemConverter : Converter<PendingTransaction,
override fun convert(value: PendingTransaction): BalanceItem? {
return BalanceItem(
groupId = value.groupId ?: return null,
token = value.token,
type = value.type,
amount = value.amount,
rawCurrencyId = value.rawCurrencyId,

View file

@ -0,0 +1,22 @@
package com.tangem.data.staking.converters
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO.BalanceTypeDTO
import com.tangem.domain.staking.model.stakekit.BalanceType
import com.tangem.utils.converter.Converter
internal class BalanceTypeConverter : Converter<BalanceTypeDTO, BalanceType> {
override fun convert(value: BalanceTypeDTO): BalanceType {
return when (value) {
BalanceTypeDTO.AVAILABLE -> BalanceType.AVAILABLE
BalanceTypeDTO.STAKED -> BalanceType.STAKED
BalanceTypeDTO.PREPARING -> BalanceType.PREPARING
BalanceTypeDTO.LOCKED -> BalanceType.LOCKED
BalanceTypeDTO.UNSTAKING -> BalanceType.UNSTAKING
BalanceTypeDTO.UNLOCKING -> BalanceType.UNLOCKING
BalanceTypeDTO.UNSTAKED -> BalanceType.UNSTAKED
BalanceTypeDTO.REWARDS -> BalanceType.REWARDS
BalanceTypeDTO.UNKNOWN -> BalanceType.UNKNOWN
}
}
}

View file

@ -1,22 +0,0 @@
package com.tangem.data.staking.converters
import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYieldDTO
import com.tangem.domain.staking.model.StakingToken
import com.tangem.domain.staking.model.StakingTokenWithYield
import com.tangem.utils.converter.Converter
class StakingTokenConverter : Converter<TokenWithYieldDTO, StakingTokenWithYield> {
override fun convert(value: TokenWithYieldDTO): StakingTokenWithYield {
return StakingTokenWithYield(
token = StakingToken(
name = value.token.name,
symbol = value.token.symbol,
decimals = value.token.decimals,
contractAddress = value.token.address,
coinGeckoId = value.token.coinGeckoId,
),
availableYieldIds = value.availableYieldIds,
)
}
}

View file

@ -2,13 +2,17 @@ package com.tangem.data.staking.converters
import com.tangem.data.staking.converters.action.PendingActionConverter
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.domain.staking.model.stakekit.*
import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.YieldBalanceItem
import com.tangem.utils.converter.Converter
internal class YieldBalanceConverter : Converter<YieldBalanceWrapperDTO, YieldBalance> {
private val pendingActionConverter by lazy(LazyThreadSafetyMode.NONE) { PendingActionConverter() }
private val networkTypeConverter by lazy(LazyThreadSafetyMode.NONE) { StakingNetworkTypeConverter() }
private val tokenConverter by lazy(LazyThreadSafetyMode.NONE) { TokenConverter(networkTypeConverter) }
private val balanceTypeConverter by lazy(LazyThreadSafetyMode.NONE) { BalanceTypeConverter() }
override fun convert(value: YieldBalanceWrapperDTO): YieldBalance {
return if (value.balances.isEmpty()) {
YieldBalance.Empty
@ -19,7 +23,8 @@ internal class YieldBalanceConverter : Converter<YieldBalanceWrapperDTO, YieldBa
items = value.balances.map { item ->
BalanceItem(
groupId = item.groupId,
type = BalanceType.valueOf(item.type.name),
token = tokenConverter.convert(item.tokenDTO),
type = balanceTypeConverter.convert(item.type),
amount = item.amount,
rawCurrencyId = item.tokenDTO.coinGeckoId,
// tron-specific. operates validatorAddresses instead of validatorAddress

View file

@ -93,29 +93,6 @@ internal object TokensDataModule {
return DefaultMarketCryptoCurrencyRepository(expressAssetsStore, coroutineDispatcherProvider)
}
@Provides
@Singleton
fun providesTokensListRepository(
tangemTechApi: TangemTechApi,
dispatchers: CoroutineDispatcherProvider,
quotesRepository: QuotesRepository,
): TokensListRepository {
return DefaultTokensListRepository(
tangemTechApi = tangemTechApi,
dispatchers = dispatchers,
quotesRepository = quotesRepository,
)
}
@Provides
@Singleton
fun provideCardNetworksRepository(
userWalletsStore: UserWalletsStore,
dispatchers: CoroutineDispatcherProvider,
): NetworksCompatibilityRepository {
return DefaultNetworksCompatibilityRepository(userWalletsStore = userWalletsStore, dispatchers = dispatchers)
}
@Provides
@Singleton
fun provideCurrencyChecksRepository(walletManagersFacade: WalletManagersFacade): CurrencyChecksRepository {

View file

@ -26,8 +26,6 @@ import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.core.error.DataError
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.lce.lceFlow
@ -138,7 +136,7 @@ internal class DefaultCurrenciesRepository(
cryptoCurrencyFactory.createCoin(
blockchain = getBlockchain(networkId = it.network.id),
extraDerivationPath = it.network.derivationPath.value,
derivationStyleProvider = getUserWallet(userWalletId).scanResponse.derivationStyleProvider,
scanResponse = getUserWallet(userWalletId).scanResponse,
)
}
.distinct()
@ -376,24 +374,6 @@ internal class DefaultCurrenciesRepository(
}.cancellable()
}
override fun getMissedAddressesCryptoCurrencies(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
return channelFlow {
ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true)
val userWallet = getUserWallet(userWalletId = userWalletId)
getMultiCurrencyWalletCurrencies(userWallet = userWallet)
.map {
it.filter { currency ->
val blockchain = Blockchain.fromId(id = currency.network.id.value)
val derivationPath = currency.network.derivationPath.value
derivationPath != null && !userWallet.scanResponse.hasDerivation(blockchain, derivationPath)
}
}
.collectLatest(::send)
}
}
override fun isSendBlockedByPendingTransactions(
cryptoCurrencyStatus: CryptoCurrencyStatus,
coinStatus: CryptoCurrencyStatus?,
@ -475,7 +455,7 @@ internal class DefaultCurrenciesRepository(
token = token,
networkId = networkId,
extraDerivationPath = null,
derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider,
scanResponse = userWallet.scanResponse,
) ?: error("Unable to create token")
}

View file

@ -1,101 +0,0 @@
package com.tangem.data.tokens.repository
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.common.card.EllipticCurve
import com.tangem.data.common.currency.getNetwork
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.extensions.*
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.NetworksCompatibilityRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
internal class DefaultNetworksCompatibilityRepository(
private val userWalletsStore: UserWalletsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : NetworksCompatibilityRepository {
/**
* @return returns true if either the network is not Solana (check is not relevant) or if it is Solana and
* UserWallet supports tokens on Solana Network
*/
@Throws(IllegalArgumentException::class)
override suspend fun areSolanaTokensSupportedIfRelevant(networkId: String, userWalletId: UserWalletId): Boolean {
return withContext(dispatchers.io) {
val scanResponse = getWalletOrThrow(userWalletId).scanResponse
val blockchain = getBlockchainOrThrow(networkId)
val blockchainsSupportingTokens = scanResponse.card.supportedTokens(scanResponse.cardTypesResolver)
blockchain != Blockchain.Solana || blockchainsSupportingTokens.contains(Blockchain.Solana)
}
}
@Throws(IllegalArgumentException::class)
override suspend fun areTokensSupportedByNetwork(networkId: String, userWalletId: UserWalletId): Boolean {
return withContext(dispatchers.io) {
val scanResponse = getWalletOrThrow(userWalletId).scanResponse
val blockchain = getBlockchainOrThrow(networkId)
val blockchainsSupportingTokens = scanResponse.card.supportedTokens(scanResponse.cardTypesResolver)
scanResponse.card.canHandleToken(
supportedTokens = blockchainsSupportingTokens,
blockchain = blockchain,
cardTypesResolver = scanResponse.cardTypesResolver,
)
}
}
@Throws(IllegalArgumentException::class)
override suspend fun isNetworkSupported(networkId: String, userWalletId: UserWalletId): Boolean {
return withContext(dispatchers.io) {
val scanResponse = getWalletOrThrow(userWalletId).scanResponse
val blockchain = getBlockchainOrThrow(networkId)
scanResponse.card.canHandleBlockchain(
blockchain = blockchain,
cardTypesResolver = scanResponse.cardTypesResolver,
)
}
}
@Throws(IllegalArgumentException::class)
override suspend fun getSupportedNetworks(userWalletId: UserWalletId): List<Network> {
val scanResponse = getWalletOrThrow(userWalletId).scanResponse
return Blockchain.entries
.filter { blockchain ->
scanResponse.card.supportedBlockchains(scanResponse.cardTypesResolver).contains(blockchain)
}
.sortedBy(Blockchain::fullName)
.mapNotNull { blockchain ->
getNetwork(blockchain, null, scanResponse.derivationStyleProvider)
}
}
override suspend fun requiresHardenedDerivationOnly(networkId: String, userWalletId: UserWalletId): Boolean {
val scanResponse = getWalletOrThrow(userWalletId).scanResponse
val config = CardConfig.createConfig(scanResponse.card)
val blockchain = Blockchain.fromNetworkId(networkId) ?: return false
return config.primaryCurve(blockchain) == EllipticCurve.Ed25519Slip0010 &&
scanResponse.cardTypesResolver.isWallet2()
}
override fun areTokensSupportedByNetwork(networkId: String): Boolean {
return Blockchain.fromNetworkId(networkId)?.canHandleTokens() ?: false
}
private suspend fun getWalletOrThrow(userWalletId: UserWalletId): UserWallet {
return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Requested UserWallet not found"
}
}
private fun getBlockchainOrThrow(networkId: String): Blockchain {
return requireNotNull(Blockchain.fromNetworkId(networkId)) {
"Requested network not found"
}
}
}

View file

@ -1,79 +0,0 @@
package com.tangem.data.tokens.repository
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.tokens.paging.CoinsPagingSource
import com.tangem.data.tokens.utils.FoundTokenConverter
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.tokens.model.FoundToken
import com.tangem.domain.tokens.model.Token
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.tokens.repository.TokensListRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
/**
* Default repository implementation for managing operations related to a complete set of tokens
*
* @property tangemTechApi Tangem Tech API
* @property dispatchers coroutine dispatchers provider
* @property quotesRepository responsible for providing cryptocurrency quotes data
*
*/
internal class DefaultTokensListRepository(
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
private val quotesRepository: QuotesRepository,
) : TokensListRepository {
override fun getTokens(searchText: String?): Flow<PagingData<Token>> {
return Pager(
config = PagingConfig(
pageSize = 100,
prefetchDistance = 70,
enablePlaceholders = false,
),
pagingSourceFactory = {
CoinsPagingSource(
api = tangemTechApi,
dispatchers = dispatchers,
searchText = searchText,
quotesRepository = quotesRepository,
)
},
).flow
}
override suspend fun findToken(contractAddress: String, networkId: String): FoundToken? {
return withContext(dispatchers.io) {
val foundCoin = tangemTechApi.getCoins(
contractAddress = contractAddress,
networkIds = networkId,
).getOrThrow().coins.firstNotNullOfOrNull { coin ->
val tokenNetwork = coin.networks.filter { network ->
network.contractAddress != null && network.decimalCount != null &&
network.contractAddress?.equals(contractAddress, ignoreCase = true) == true &&
networkId == network.networkId
}
if (tokenNetwork.isNotEmpty()) {
coin.copy(networks = tokenNetwork)
} else {
null
}
}
foundCoin?.let { FoundTokenConverter.convert(foundCoin) }
}
}
override fun validateAddress(contractAddress: String, networkId: String): Boolean {
return when (val blockchain = Blockchain.fromNetworkId(networkId) ?: Blockchain.Unknown) {
Blockchain.Unknown, Blockchain.Binance, Blockchain.BinanceTestnet -> true
else -> blockchain.validateAddress(contractAddress)
}
}
}

View file

@ -4,7 +4,6 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
@ -14,7 +13,6 @@ internal class CardCryptoCurrenciesFactory(private val demoConfig: DemoConfig) {
private val cryptoCurrencyFactory = CryptoCurrencyFactory()
fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List<CryptoCurrency.Coin> {
val cardDerivationStyleProvider = scanResponse.derivationStyleProvider
val card = scanResponse.card
var blockchains = if (demoConfig.isDemoCardId(card.cardId)) {
@ -31,20 +29,19 @@ internal class CardCryptoCurrenciesFactory(private val demoConfig: DemoConfig) {
cryptoCurrencyFactory.createCoin(
blockchain = it,
extraDerivationPath = null,
derivationStyleProvider = cardDerivationStyleProvider,
scanResponse = scanResponse,
)
}
}
fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency {
val cardDerivationStyleProvider = scanResponse.derivationStyleProvider
val resolver = scanResponse.cardTypesResolver
val blockchain = resolver.getBlockchain()
val coin = cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = null,
derivationStyleProvider = cardDerivationStyleProvider,
scanResponse = scanResponse,
)
requireNotNull(coin) { "Coin for the single currency card cannot be null" }
@ -53,7 +50,7 @@ internal class CardCryptoCurrenciesFactory(private val demoConfig: DemoConfig) {
sdkToken = token,
blockchain = blockchain,
extraDerivationPath = null,
derivationStyleProvider = cardDerivationStyleProvider,
scanResponse = scanResponse,
)
}
@ -61,14 +58,13 @@ internal class CardCryptoCurrenciesFactory(private val demoConfig: DemoConfig) {
}
fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List<CryptoCurrency> {
val cardDerivationStyleProvider = scanResponse.derivationStyleProvider
val resolver = scanResponse.cardTypesResolver
val blockchain = resolver.getBlockchain()
val coin = cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = null,
derivationStyleProvider = cardDerivationStyleProvider,
scanResponse = scanResponse,
)
requireNotNull(coin) { "Coin for the single currency card cannot be null" }
@ -77,7 +73,7 @@ internal class CardCryptoCurrenciesFactory(private val demoConfig: DemoConfig) {
sdkToken = token,
blockchain = blockchain,
extraDerivationPath = null,
derivationStyleProvider = cardDerivationStyleProvider,
scanResponse = scanResponse,
)
}