Updated on 2026-08-14

This commit is contained in:
Tangem 2023-08-29 11:38:13 +03:00
commit 64fab92d06
231 changed files with 5317 additions and 1797 deletions

View file

@ -10,9 +10,9 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEmpty
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.joda.time.Duration
import timber.log.Timber
@ -27,11 +27,18 @@ internal class DefaultAppCurrencyRepository(
private val appCurrencyConverter = AppCurrencyConverter()
override fun getSelectedAppCurrency(): Flow<AppCurrency> {
return selectedAppCurrencyStore.get()
.onEmpty { fetchDefaultAppCurrency() }
.map(appCurrencyConverter::convert)
.flowOn(dispatchers.io)
override fun getSelectedAppCurrency(): Flow<AppCurrency> = channelFlow {
launch(dispatchers.io) {
selectedAppCurrencyStore.get()
.map(appCurrencyConverter::convert)
.collect(::send)
}
launch(dispatchers.io) {
if (selectedAppCurrencyStore.isEmpty()) {
fetchDefaultAppCurrency()
}
}
}
override suspend fun getAvailableAppCurrencies(): List<AppCurrency> {

1
data/app-theme/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,34 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
android {
namespace = "com.tangem.data.apptheme"
}
dependencies {
/** Project - Domain */
implementation(projects.domain.core)
implementation(projects.domain.appTheme)
implementation(projects.domain.appTheme.models)
/** Project - Data */
implementation(projects.core.datasource)
implementation(projects.data.common)
/** Project - Utils */
implementation(projects.core.utils)
/** DI */
implementation(deps.hilt.core)
kapt(deps.hilt.kapt)
/** Other */
implementation(deps.kotlin.coroutines)
implementation(deps.timber)
implementation(deps.jodatime)
}

View file

@ -0,0 +1,19 @@
package com.tangem.data.apptheme
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
internal class MockAppThemeModeRepository : AppThemeModeRepository {
private val appThemeModeFlow = MutableStateFlow(AppThemeMode.DEFAULT)
override fun getAppThemeMode(): Flow<AppThemeMode> {
return appThemeModeFlow
}
override suspend fun changeAppThemeMode(mode: AppThemeMode) {
appThemeModeFlow.value = mode
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.data.apptheme.di
import com.tangem.data.apptheme.MockAppThemeModeRepository
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
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 AppThemeModeDataModule {
@Provides
@Singleton
fun provideAppThemeModeRepository(): AppThemeModeRepository {
return MockAppThemeModeRepository()
}
}

View file

@ -15,4 +15,8 @@ internal class DefaultSettingsRepository(
preferencesDataSource.appRatingLaunchObserver.isReadyToShow()
}
}
override suspend fun shouldShowSaveUserWalletScreen(): Boolean {
return withContext(dispatchers.io) { preferencesDataSource.shouldShowSaveUserWalletScreen }
}
}

View file

@ -8,8 +8,8 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.core.error.DataError
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.core.error.DataError
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -164,7 +164,7 @@ internal class DefaultCurrenciesRepository(
tangemTechApi.saveUserTokens(userWallet.walletId.stringValue, response)
} else {
throw error
Timber.e(error, "Unable to fetch currencies for: ${userWallet.walletId}")
}
}

View file

@ -88,7 +88,7 @@ internal class DefaultNetworksRepository(
private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, networkId: Network.ID) {
val currencies = getCurrencies(userWalletId)
.asSequence()
.filter { it.networkId == networkId }
.filter { it.network.id == networkId }
val result = walletManagersFacade.update(
userWalletId = userWalletId,

View file

@ -1,11 +1,12 @@
package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token as SdkToken
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.tokens.models.CryptoCurrency
import timber.log.Timber
import com.tangem.blockchain.common.Token as SdkToken
// FIXME: Make internal
class CryptoCurrencyFactory {
fun createToken(
@ -19,9 +20,10 @@ class CryptoCurrencyFactory {
}
val id = getTokenId(blockchain, sdkToken)
return CryptoCurrency.Token(
id = id,
networkId = getNetworkId(blockchain),
network = getNetwork(blockchain) ?: return null,
name = sdkToken.name,
symbol = sdkToken.symbol,
iconUrl = getTokenIconUrl(blockchain, sdkToken),
@ -29,8 +31,6 @@ class CryptoCurrencyFactory {
isCustom = isCustomToken(id),
contractAddress = sdkToken.contractAddress,
derivationPath = getDerivationPath(blockchain, derivationStyleProvider),
blockchainName = blockchain.fullName,
standardType = getTokenStandardType(blockchain, sdkToken),
)
}
@ -42,7 +42,7 @@ class CryptoCurrencyFactory {
return CryptoCurrency.Coin(
id = getCoinId(blockchain),
networkId = getNetworkId(blockchain),
network = getNetwork(blockchain) ?: return null,
name = blockchain.fullName,
symbol = blockchain.currency,
iconUrl = getCoinIconUrl(blockchain),

View file

@ -3,22 +3,13 @@ package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.tokens.models.Network
import com.tangem.utils.converter.Converter
import timber.log.Timber
internal class NetworkConverter : Converter<Network.ID, Network?> {
override fun convert(value: Network.ID): Network? {
val blockchain = Blockchain.fromId(value.value)
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to convert Unknown blockchain to the domain network model")
return null
}
return Network(
id = value,
name = blockchain.fullName,
)
return getNetwork(blockchain)
}
override fun convertList(input: Collection<Network.ID>): List<Network> {

View file

@ -0,0 +1,29 @@
package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.tokens.models.Network
import timber.log.Timber
internal fun getNetwork(blockchain: Blockchain): Network? {
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to convert Unknown blockchain to the domain network model")
return null
}
return Network(
id = Network.ID(blockchain.id),
name = blockchain.fullName,
isTestnet = blockchain.isTestnet(),
standardType = getNetworkStandardType(blockchain),
)
}
private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType {
return when (blockchain) {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.StandardType.ERC20
Blockchain.BSC, Blockchain.BSCTestnet -> Network.StandardType.BEP20
Blockchain.Binance, Blockchain.BinanceTestnet -> Network.StandardType.BEP2
Blockchain.Tron, Blockchain.TronTestnet -> Network.StandardType.TRC20
else -> Network.StandardType.Unspecified(blockchain.name)
}
}

View file

@ -1,10 +1,14 @@
package com.tangem.data.tokens.utils
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.model.PendingTransaction
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import timber.log.Timber
import java.math.BigDecimal
internal class NetworkStatusFactory {
@ -19,10 +23,18 @@ internal class NetworkStatusFactory {
value = when (result) {
is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation
is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable
is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount(result.amountToCreateAccount)
is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount(
address = getNetworkAddress(result.defaultAddress, result.addresses),
amountToCreateAccount = result.amountToCreateAccount,
)
is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified(
amounts = formatAmounts(result.tokensAmounts, currencies),
hasTransactionsInProgress = result.hasTransactionsInProgress,
address = getNetworkAddress(result.defaultAddress, result.addresses),
amounts = formatAmounts(result.currenciesAmounts, currencies),
pendingTransactions = formatTransactions(
networksAddresses = result.addresses,
transactions = result.currentTransactions,
currencies = currencies,
),
)
},
)
@ -39,13 +51,91 @@ internal class NetworkStatusFactory {
is CryptoCurrencyAmount.Coin -> currencies.singleOrNull { it is CryptoCurrency.Coin }
is CryptoCurrencyAmount.Token -> currencies.firstOrNull {
it is CryptoCurrency.Token &&
it.id.rawCurrencyId == amount.id &&
it.id.rawCurrencyId == amount.tokenId &&
it.contractAddress == amount.tokenContractAddress
}
}
currency?.id?.let { it to amount.value }
if (currency == null) {
Timber.e("Unable to find cryptocurrency for amount: $amount")
null
} else {
currency.id to amount.value
}
}
.toMap()
}
private fun formatTransactions(
networksAddresses: Set<String>,
transactions: Set<CryptoCurrencyTransaction>,
currencies: Set<CryptoCurrency>,
): Map<CryptoCurrency.ID, Set<PendingTransaction>> {
if (transactions.isEmpty()) return emptyMap()
return currencies
.asSequence()
.map { currency ->
val currencyTransactions = when (currency) {
is CryptoCurrency.Coin -> transactions.filterTo(hashSetOf()) { transaction ->
transaction is CryptoCurrencyTransaction.Coin
}
is CryptoCurrency.Token -> transactions.filterTo(hashSetOf()) { transaction ->
transaction is CryptoCurrencyTransaction.Token &&
transaction.tokenId == currency.id.rawCurrencyId &&
transaction.tokenContractAddress == currency.contractAddress
}
}
currency.id to createCurrentTransactions(networksAddresses, currencyTransactions)
}
.toMap()
}
private fun createCurrentTransactions(
networksAddresses: Set<String>,
transactions: Set<CryptoCurrencyTransaction>,
): Set<PendingTransaction> {
return transactions.mapNotNullTo(hashSetOf()) { createCurrentTransaction(networksAddresses, it) }
}
private fun createCurrentTransaction(
networksAddresses: Set<String>,
transaction: CryptoCurrencyTransaction,
): PendingTransaction? {
val direction = when {
transaction.toAddress in networksAddresses -> PendingTransaction.Direction.Incoming(
fromAddress = transaction.fromAddress,
)
transaction.fromAddress in networksAddresses -> PendingTransaction.Direction.Outgoing(
toAddress = transaction.toAddress,
)
else -> {
Timber.e(
"""
Unable to find transaction direction
|- To address: ${transaction.toAddress}
|- From address: ${transaction.fromAddress}
|- Network addresses: $networksAddresses
""".trimIndent(),
)
return null
}
}
return PendingTransaction(
amount = transaction.amount,
direction = direction,
sentAt = transaction.sentAt,
)
}
private fun getNetworkAddress(defaultAddress: String, availableAddresses: Set<String>): NetworkAddress {
return if (availableAddresses.size != 1) {
NetworkAddress.Selectable(defaultAddress, availableAddresses)
} else {
NetworkAddress.Single(defaultAddress)
}
}
}

View file

@ -59,10 +59,10 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) {
}
}
private fun createCoin(blockchain: Blockchain, responseToken: UserTokensResponse.Token): CryptoCurrency.Coin {
private fun createCoin(blockchain: Blockchain, responseToken: UserTokensResponse.Token): CryptoCurrency.Coin? {
return CryptoCurrency.Coin(
id = getCoinId(blockchain),
networkId = getNetworkId(blockchain),
network = getNetwork(blockchain) ?: return null,
name = responseToken.name,
symbol = responseToken.symbol,
decimals = responseToken.decimals,
@ -71,12 +71,12 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) {
)
}
private fun createToken(blockchain: Blockchain, sdkToken: Token, derivationPath: String?): CryptoCurrency.Token {
private fun createToken(blockchain: Blockchain, sdkToken: Token, derivationPath: String?): CryptoCurrency.Token? {
val id = getTokenId(blockchain, sdkToken)
return CryptoCurrency.Token(
id = id,
networkId = getNetworkId(blockchain),
network = getNetwork(blockchain) ?: return null,
name = sdkToken.name,
symbol = sdkToken.symbol,
decimals = sdkToken.decimals,
@ -84,8 +84,6 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) {
iconUrl = getTokenIconUrl(blockchain, sdkToken),
contractAddress = sdkToken.contractAddress,
isCustom = isCustomToken(id),
blockchainName = blockchain.fullName,
standardType = getTokenStandardType(blockchain, sdkToken),
)
}
}

View file

@ -5,7 +5,6 @@ import com.tangem.blockchain.common.IconsUtil
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.CryptoCurrency.ID
import com.tangem.domain.tokens.models.Network
import com.tangem.blockchain.common.Token as SdkToken
@ -31,12 +30,6 @@ internal fun getBlockchain(networkId: Network.ID): Blockchain {
return Blockchain.fromId(networkId.value)
}
internal fun getNetworkId(blockchain: Blockchain): Network.ID {
val value = blockchain.id
return Network.ID(value)
}
internal fun getCoinId(blockchain: Blockchain): ID {
return getTokenOrCoinId(blockchain, token = null)
}
@ -45,16 +38,6 @@ internal fun getTokenId(blockchain: Blockchain, token: SdkToken): ID {
return getTokenOrCoinId(blockchain, token)
}
internal fun getTokenStandardType(blockchain: Blockchain, token: SdkToken): CryptoCurrency.StandardType {
return when (blockchain) {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> CryptoCurrency.StandardType.ERC20
Blockchain.BSC, Blockchain.BSCTestnet -> CryptoCurrency.StandardType.BEP20
Blockchain.Binance, Blockchain.BinanceTestnet -> CryptoCurrency.StandardType.BEP2
Blockchain.Tron, Blockchain.TronTestnet -> CryptoCurrency.StandardType.TRC20
else -> CryptoCurrency.StandardType.Unspecified(token.name)
}
}
internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? {
val tokenId = token.id
@ -83,7 +66,7 @@ private fun getTokenOrCoinId(blockchain: Blockchain, token: SdkToken?): ID {
else -> TOKEN_ID_PREFIX to CurrencyIdSuffix(rawId = sdkTokenId)
}
return ID(prefix, getNetworkId(blockchain), suffix)
return ID(prefix, Network.ID(blockchain.id), suffix)
}
private fun getTokenIconUrlFromDefaultHost(tokenId: String): String {

View file

@ -27,7 +27,7 @@ internal class UserTokensResponseFactory {
}
private fun createResponseToken(currency: CryptoCurrency): UserTokensResponse.Token {
val blockchain = getBlockchain(currency.networkId)
val blockchain = getBlockchain(currency.network.id)
return UserTokensResponse.Token(
id = currency.id.rawCurrencyId,

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

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,21 @@
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.wallet"
}
dependencies {
implementation(projects.core.utils)
implementation(projects.data.source.preferences)
implementation(projects.domain.wallets)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,16 @@
package com.tangem.data.wallets
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
internal class DefaultWalletsRepository(
private val preferencesDataSource: PreferencesDataSource,
private val dispatchers: CoroutineDispatcherProvider,
) : WalletsRepository {
override suspend fun shouldSaveUserWallets(): Boolean {
return withContext(dispatchers.io) { preferencesDataSource.shouldSaveUserWallets }
}
}

View file

@ -0,0 +1,28 @@
package com.tangem.data.wallets.di
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.data.wallets.DefaultWalletsRepository
import com.tangem.domain.wallets.repository.WalletsRepository
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)
object WalletsDataModule {
@Provides
@Singleton
fun providesWalletsRepository(
preferencesDataSource: PreferencesDataSource,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
): WalletsRepository {
return DefaultWalletsRepository(
preferencesDataSource = preferencesDataSource,
dispatchers = coroutineDispatcherProvider,
)
}
}