Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-12 18:09:27 +03:00
commit c1cc66fea8
162 changed files with 3408 additions and 1585 deletions

View file

@ -10,7 +10,6 @@ import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -70,7 +69,6 @@ internal object ManageTokensDomainModule {
customTokensRepository: CustomTokensRepository,
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
derivationsRepository: DerivationsRepository,
stakingRepository: StakingRepository,
quotesRepository: QuotesRepository,
@ -83,7 +81,6 @@ internal object ManageTokensDomainModule {
customTokensRepository = customTokensRepository,
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
derivationsRepository = derivationsRepository,
stakingRepository = stakingRepository,
quotesRepository = quotesRepository,

View file

@ -11,7 +11,6 @@ import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import dagger.Module
@ -70,7 +69,6 @@ object MarketsDomainModule {
derivationsRepository: DerivationsRepository,
marketsTokenRepository: MarketsTokenRepository,
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
quotesRepository: QuotesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
@ -82,7 +80,6 @@ object MarketsDomainModule {
derivationsRepository = derivationsRepository,
marketsTokenRepository = marketsTokenRepository,
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
quotesRepository = quotesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,

View file

@ -37,7 +37,6 @@ internal object TokensDomainModule {
@Singleton
fun provideAddCryptoCurrenciesUseCase(
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
quotesRepository: QuotesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
@ -47,7 +46,6 @@ internal object TokensDomainModule {
): AddCryptoCurrenciesUseCase {
return AddCryptoCurrenciesUseCase(
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
quotesRepository = quotesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
@ -62,7 +60,6 @@ internal object TokensDomainModule {
fun provideFetchTokenListUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
@ -71,7 +68,6 @@ internal object TokensDomainModule {
): FetchTokenListUseCase {
return FetchTokenListUseCase(
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
@ -121,8 +117,8 @@ internal object TokensDomainModule {
fun provideGetCurrencyUseCase(
baseCurrencyStatusOperations: BaseCurrencyStatusOperations,
dispatchers: CoroutineDispatcherProvider,
): GetCurrencyStatusUpdatesUseCase {
return GetCurrencyStatusUpdatesUseCase(
): GetSingleCryptoCurrencyStatusUseCase {
return GetSingleCryptoCurrencyStatusUseCase(
currencyStatusOperations = baseCurrencyStatusOperations,
dispatchers = dispatchers,
)
@ -162,24 +158,11 @@ internal object TokensDomainModule {
)
}
@Provides
@Singleton
fun provideGetPrimaryCurrencyUseCase(
currencyStatusOperations: BaseCurrencyStatusOperations,
dispatchers: CoroutineDispatcherProvider,
): GetPrimaryCurrencyStatusUpdatesUseCase {
return GetPrimaryCurrencyStatusUpdatesUseCase(
currencyStatusOperations = currencyStatusOperations,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideFetchCurrencyStatusUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
@ -188,7 +171,6 @@ internal object TokensDomainModule {
): FetchCurrencyStatusUseCase {
return FetchCurrencyStatusUseCase(
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
@ -203,7 +185,6 @@ internal object TokensDomainModule {
fun provideFetchCardTokenListUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
@ -212,7 +193,6 @@ internal object TokensDomainModule {
): FetchCardTokenListUseCase {
return FetchCardTokenListUseCase(
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
@ -222,14 +202,6 @@ internal object TokensDomainModule {
)
}
@Provides
@Singleton
fun providesGetCryptoCurrencyStatusSyncUseCase(
currencyStatusOperations: BaseCurrencyStatusOperations,
): GetCryptoCurrencyStatusSyncUseCase {
return GetCryptoCurrencyStatusSyncUseCase(currencyStatusOperations)
}
@Provides
@Singleton
fun provideGetCryptoCurrencyUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrencyUseCase {
@ -328,14 +300,10 @@ internal object TokensDomainModule {
@Provides
@Singleton
fun provideUpdateDelayedCurrencyStatusUseCase(
networksRepository: NetworksRepository,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): UpdateDelayedNetworkStatusUseCase {
return UpdateDelayedNetworkStatusUseCase(
networksRepository = networksRepository,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -413,7 +381,6 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
quotesRepositoryV2: QuotesRepositoryV2,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
@ -427,7 +394,6 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
quotesRepositoryV2 = quotesRepositoryV2,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
@ -447,7 +413,6 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
quotesRepositoryV2: QuotesRepositoryV2,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
@ -461,7 +426,6 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
quotesRepositoryV2 = quotesRepositoryV2,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,

View file

@ -4,9 +4,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.usecase.*
@ -43,7 +41,6 @@ internal object TransactionDomainModule {
transactionRepository: TransactionRepository,
walletManagersFacade: WalletManagersFacade,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): SendTransactionUseCase {
return SendTransactionUseCase(
demoConfig = DemoConfig(),
@ -51,7 +48,6 @@ internal object TransactionDomainModule {
transactionRepository = transactionRepository,
walletManagersFacade = walletManagersFacade,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -61,17 +57,13 @@ internal object TransactionDomainModule {
cardSdkConfigRepository: CardSdkConfigRepository,
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): AssociateAssetUseCase {
return AssociateAssetUseCase(
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}

View file

@ -6,6 +6,8 @@ import com.tangem.domain.transaction.usecase.ParseSharedAddressUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate
import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
import com.tangem.domain.wallets.repository.WalletsRepository
@ -28,6 +30,17 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
internal object WalletsDomainModule {
@Provides
fun providesUserWalletsSyncDelegate(
userWalletsListManager: UserWalletsListManager,
dispatchers: CoroutineDispatcherProvider,
): UserWalletsSyncDelegate {
return DefaultUserWalletsSyncDelegate(
userWalletsListManager = userWalletsListManager,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun providesGetWalletsUseCase(userWalletsListManager: UserWalletsListManager): GetWalletsUseCase {
@ -102,10 +115,13 @@ internal object WalletsDomainModule {
@Provides
@Singleton
fun providesRenameWalletUseCase(
userWalletsListManager: UserWalletsListManager,
dispatchers: CoroutineDispatcherProvider,
walletsRepository: WalletsRepository,
userWalletsSyncDelegate: UserWalletsSyncDelegate,
): RenameWalletUseCase {
return RenameWalletUseCase(userWalletsListManager = userWalletsListManager, dispatchers = dispatchers)
return RenameWalletUseCase(
walletsRepository = walletsRepository,
userWalletsSyncDelegate = userWalletsSyncDelegate,
)
}
@Provides
@ -197,4 +213,16 @@ internal object WalletsDomainModule {
nftFeatureToggles = nftFeatureToggles,
)
}
@Provides
@Singleton
fun providesUpdateRemoteWalletsInfoUseCase(
walletsRepository: WalletsRepository,
userWalletsSyncDelegate: UserWalletsSyncDelegate,
): UpdateRemoteWalletsInfoUseCase {
return UpdateRemoteWalletsInfoUseCase(
walletsRepository = walletsRepository,
userWalletsSyncDelegate = userWalletsSyncDelegate,
)
}
}

View file

@ -7,9 +7,6 @@ internal class DefaultTokensFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : TokensFeatureToggles {
override val isNetworksLoadingRefactoringEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "NETWORKS_LOADING_REFACTORING_ENABLED")
override val isQuotesLoadingRefactoringEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "QUOTES_LOADING_REFACTORING_ENABLED")

View file

@ -13,6 +13,7 @@ import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.model.WcPairRequest
import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
@ -64,14 +65,18 @@ internal class DefaultLegacyWalletConnectRepositoryFacade constructor(
if (isNewWc) stub.updateSessions() else legacy.updateSessions()
}
override fun pair(uri: String, source: SourceType) {
override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) {
val src = when (source) {
SourceType.QR -> WcPairRequest.Source.QR
SourceType.DEEPLINK -> WcPairRequest.Source.DEEPLINK
SourceType.CLIPBOARD -> WcPairRequest.Source.CLIPBOARD
SourceType.ETC -> WcPairRequest.Source.ETC
}
if (isNewWc) wcPairService.pair(WcPairRequest(uri, src)) else legacy.pair(uri, source)
if (isNewWc) {
wcPairService.pair(WcPairRequest(uri = uri, source = src, userWalletId = userWalletId))
} else {
legacy.pair(userWalletId = userWalletId, uri = uri, source = source)
}
}
override fun disconnect(topic: String) {
@ -110,7 +115,7 @@ internal class LegacyWalletConnectRepositoryStub : LegacyWalletConnectRepository
override fun updateSessions() = Unit
override fun pair(uri: String, source: SourceType) = Unit
override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) = Unit
override fun disconnect(topic: String) = Unit
@ -405,7 +410,7 @@ internal class DefaultLegacyWalletConnectRepository(
this.userNamespaces = userNamespaces
}
override fun pair(uri: String, source: SourceType) {
override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) {
analyticsHandler.send(WalletConnect.NewSessionInitiated(source = source))
WalletKit.pair(
params = Wallet.Params.Pair(uri),

View file

@ -1,6 +1,7 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.domain.walletconnect2.domain.models.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType
import kotlinx.coroutines.flow.Flow
@ -19,7 +20,7 @@ interface LegacyWalletConnectRepository {
fun updateSessions()
fun pair(uri: String, source: SourceType)
fun pair(userWalletId: UserWalletId, uri: String, source: SourceType)
fun disconnect(topic: String)

View file

@ -12,6 +12,7 @@ import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsReposit
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.filterNotNull
@ -139,7 +140,11 @@ class WalletConnectInteractor(
if (deeplinkStack.empty()) return
val lastDeeplink = deeplinkStack.pop()
val action = WalletConnectAction
.OpenSession(lastDeeplink, WalletConnectAction.OpenSession.SourceType.DEEPLINK)
.OpenSession(
wcUri = lastDeeplink,
source = WalletConnectAction.OpenSession.SourceType.DEEPLINK,
userWalletId = UserWalletId(userWalletId),
)
store.dispatchOnMain(action)
}.onFailure {
Timber.e("WC deeplink handling failed. $it")
@ -393,7 +398,11 @@ class WalletConnectInteractor(
}
if (isWalletConnectReadyForDeepLinks) {
val action = WalletConnectAction.OpenSession(deeplink, WalletConnectAction.OpenSession.SourceType.DEEPLINK)
val action = WalletConnectAction.OpenSession(
wcUri = deeplink,
source = WalletConnectAction.OpenSession.SourceType.DEEPLINK,
userWalletId = UserWalletId(userWalletId),
)
store.dispatchOnMain(action)
} else {
deeplinkStack.push(deeplink)

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents
@ -14,6 +15,7 @@ sealed class WalletConnectAction : Action {
data class OpenSession(
val wcUri: String,
val source: SourceType,
val userWalletId: UserWalletId,
) : WalletConnectAction() {
enum class SourceType { QR, DEEPLINK, CLIPBOARD, ETC }
}

View file

@ -63,7 +63,11 @@ class WalletConnectMiddleware {
is WalletConnectAction.OpenSession -> {
val index = action.wcUri.indexOf("@")
when (action.wcUri[index + 1]) {
'2' -> walletConnectRepository.pair(uri = action.wcUri, source = action.source)
'2' -> walletConnectRepository.pair(
uri = action.wcUri,
source = action.source,
userWalletId = action.userWalletId,
)
'1' -> {
store.dispatchOnMain(WalletConnectAction.UnsupportedDappRequest)
store.dispatchOnMain(

View file

@ -22,10 +22,10 @@ import org.rekotlin.StoreSubscriber
@Suppress("UnusedPrivateMember")
internal class DefaultWalletConnectComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
@Assisted params: WalletConnectComponent.Params,
) : WalletConnectComponent, AppComponentContext by appComponentContext, StoreSubscriber<WalletConnectState> {
private val model: WalletConnectModel = getOrCreateModel()
private val model: WalletConnectModel = getOrCreateModel(params)
private var screenState: MutableState<WalletConnectScreenState> =
mutableStateOf(model.updateState(store.state.walletConnectState))
@ -65,6 +65,9 @@ internal class DefaultWalletConnectComponent @AssistedInject constructor(
@AssistedFactory
interface Factory : WalletConnectComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultWalletConnectComponent
override fun create(
context: AppComponentContext,
params: WalletConnectComponent.Params,
): DefaultWalletConnectComponent
}
}

View file

@ -4,10 +4,12 @@ import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
@ -22,13 +24,22 @@ import javax.inject.Inject
internal class WalletConnectModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
paramsContainer: ParamsContainer,
) : Model() {
private val params = paramsContainer.require<WalletConnectComponent.Params>()
init {
modelScope.launch {
listenToQrScanningUseCase(SourceType.WALLET_CONNECT)
.getOrElse { emptyFlow() }
.map { WalletConnectAction.OpenSession(it, WalletConnectAction.OpenSession.SourceType.QR) }
.map {
WalletConnectAction.OpenSession(
wcUri = it,
source = WalletConnectAction.OpenSession.SourceType.QR,
userWalletId = params.userWalletId,
)
}
.collect { store.dispatch(it) }
}
}

View file

@ -2,7 +2,9 @@ package com.tangem.tap.features.details.ui.walletconnect.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.wallets.models.UserWalletId
interface WalletConnectComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Unit, WalletConnectComponent>
data class Params(val userWalletId: UserWalletId)
interface Factory : ComponentFactory<Params, WalletConnectComponent>
}

View file

@ -13,4 +13,10 @@ internal class DefaultAuthProvider(private val userWalletsListManager: UserWalle
override fun getCardId(): String {
return userWalletsListManager.selectedUserWalletSync?.scanResponse?.card?.cardId ?: ""
}
override fun getCardsPublicKeys(): Map<String, String> {
return userWalletsListManager.userWalletsSync.associate {
it.scanResponse.card.cardId to it.scanResponse.card.cardPublicKey.toHexString()
}
}
}

View file

@ -291,13 +291,13 @@ internal class ChildFactory @Inject constructor(
if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
createComponentChild(
context = context,
params = Unit,
params = RedesignedWalletConnectComponent.Params(route.userWalletId),
componentFactory = redesignedWalletConnectComponentFactory,
)
} else {
createComponentChild(
context = context,
params = Unit,
params = WalletConnectComponent.Params(route.userWalletId),
componentFactory = walletConnectComponentFactory,
)
}

View file

@ -122,7 +122,7 @@ sealed class AppRoute(val path: String) : Route {
}
@Serializable
data object WalletConnectSessions : AppRoute(path = "/wallet_connect_sessions")
data class WalletConnectSessions(val userWalletId: UserWalletId) : AppRoute(path = "/wallet_connect_sessions")
@Serializable
data class QrScanning(val source: Source) : AppRoute(path = "/$source/qr_scanning${source.path}") {

View file

@ -51,10 +51,6 @@
"name": "WALLET_CONNECT_REDESIGN_ENABLED",
"version": "undefined"
},
{
"name": "NETWORKS_LOADING_REFACTORING_ENABLED",
"version": "5.23.0"
},
{
"name": "QUOTES_LOADING_REFACTORING_ENABLED",
"version": "5.24.0"

View file

@ -11,4 +11,9 @@ interface AuthProvider {
fun getCardPublicKey(): String
fun getCardId(): String
/**
* Returns map where keys(cardId) associated with cardPublicKey
*/
fun getCardsPublicKeys(): Map<String, String>
}

View file

@ -172,6 +172,9 @@ interface TangemTechApi {
@GET("user-wallets/wallets/{wallet_id}")
suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse<WalletResponse>
@GET("user-wallets/wallets/by-app/{app_id}")
suspend fun getWallets(@Path("app_id") appId: String): ApiResponse<List<WalletResponse>>
// endregion
companion object {

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CardInfoBody(
@Json(name = "card_id") val cardId: String,
@Json(name = "card_public_key") val cardPublicKey: String,
)

View file

@ -6,4 +6,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WalletIdBody(
@Json(name = "id") val walletId: String,
@Json(name = "name") val name: String,
@Json(name = "cards") val cards: List<CardInfoBody>,
)

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.di.local
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.DefaultUserTokensResponseStore
import com.tangem.datasource.local.token.UserTokensResponseStore
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 LocalTokenModule {
@Provides
@Singleton
fun provideUserTokensResponseStore(appPreferencesStore: AppPreferencesStore): UserTokensResponseStore {
return DefaultUserTokensResponseStore(appPreferencesStore = appPreferencesStore)
}
}

View file

@ -3,13 +3,14 @@ package com.tangem.datasource.local.nft
import androidx.datastore.core.DataStore
import com.tangem.blockchain.nft.models.NFTAsset
import com.tangem.blockchain.nft.models.NFTCollection
import com.tangem.datasource.local.nft.custom.NFTPriceId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
internal class DefaultNFTPersistenceStore(
private val collectionsPersistenceStore: DataStore<List<NFTCollection>>,
private val pricesPersistenceStore: DataStore<Map<NFTAsset.Identifier, NFTAsset.SalePrice>>,
private val pricesPersistenceStore: DataStore<List<NFTPriceId>>,
) : NFTPersistenceStore {
override fun getCollections(): Flow<List<NFTCollection>?> = collectionsPersistenceStore.data
@ -27,11 +28,12 @@ internal class DefaultNFTPersistenceStore(
}
override fun getSalePrice(assetId: NFTAsset.Identifier): Flow<NFTAsset.SalePrice?> = pricesPersistenceStore.data
.map { it[assetId] }
.map { data -> data.associate { it.assetId to it.price }[assetId] }
override suspend fun getSalePricesSync(): Map<NFTAsset.Identifier, NFTAsset.SalePrice>? = pricesPersistenceStore
.data
.firstOrNull()
?.associate { it.assetId to it.price }
override suspend fun saveCollections(collections: List<NFTCollection>) {
collectionsPersistenceStore.updateData {
@ -41,7 +43,7 @@ internal class DefaultNFTPersistenceStore(
override suspend fun saveSalePrice(assetId: NFTAsset.Identifier, salePrice: NFTAsset.SalePrice) {
pricesPersistenceStore.updateData {
it.toMutableMap().apply { this[assetId] = salePrice }
it.toMutableList().plus(NFTPriceId(assetId = assetId, price = salePrice))
}
}

View file

@ -5,12 +5,11 @@ import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.blockchain.nft.models.NFTAsset
import com.tangem.blockchain.nft.models.NFTCollection
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.nft.custom.NFTPriceId
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.listTypes
import com.tangem.datasource.utils.mapWithCustomKeyTypes
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -48,8 +47,8 @@ class NFTPersistenceStoreFactory @Inject constructor(
// result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_eth_m4460000_prices
// result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_theopennetwork_m446070_prices
fileName = "nft_${userWalletStringId}_${networkStringId}_prices",
types = mapWithCustomKeyTypes<NFTAsset.Identifier, NFTAsset.SalePrice>(),
defaultValue = emptyMap(),
types = listTypes<NFTPriceId>(),
defaultValue = emptyList(),
),
)
}

View file

@ -0,0 +1,8 @@
package com.tangem.datasource.local.nft.custom
import com.tangem.blockchain.nft.models.NFTAsset
data class NFTPriceId(
val assetId: NFTAsset.Identifier,
val price: NFTAsset.SalePrice,
)

View file

@ -0,0 +1,25 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
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.domain.wallets.models.UserWalletId
/**
* Default implementation of [UserTokensResponseStore]
*
* @property appPreferencesStore app preferences store
*
[REDACTED_AUTHOR]
*/
internal class DefaultUserTokensResponseStore(
private val appPreferencesStore: AppPreferencesStore,
) : UserTokensResponseStore {
override suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? {
return appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue),
)
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.wallets.models.UserWalletId
/**
* Store of [UserTokensResponse]
*
[REDACTED_AUTHOR]
*/
interface UserTokensResponseStore {
/** Get [UserTokensResponse] synchronously by [userWalletId] or null */
suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse?
}

View file

@ -278,7 +278,7 @@
<string name="exchange_tokens_unavailable_tokens_header">Kann nicht gegen %s ausgetauscht werden</string>
<string name="express_by_provider">Bereitgestellt von</string>
<string name="express_cex_status_button_title">Status</string>
<string name="express_choose_providers_subtitle">Tangem bietet Token-Swaps über Drittanbieter gemäß den jeweiligen Bedingungen des jeweiligen Anbieters an.</string>
<string name="express_choose_providers_subtitle">Anbieter erleichtern Transaktionen</string>
<string name="express_choose_providers_title">Anbieter</string>
<string name="express_error_code">Es ist ein Fehler aufgetreten. Code: %s</string>
<string name="express_error_provider_amount_roundup">Fehler %1$s. Der ausgewählte Anbieter kann die angegebene Transaktion nicht verarbeiten. Bitte runde den Wert auf %2$s auf oder änder diesen.</string>
@ -319,11 +319,11 @@
<string name="express_exchange_status_verifying">Verifizierung erforderlich</string>
<string name="express_exchange_status_waiting_tx_hash">Wartet auf Transaktions-Hash</string>
<string name="express_exchange_token_list_subtitle">Liste aller Token, die deiner Wallet hinzugefügt wurden</string>
<string name="express_fetch_best_rates">Beste Preise werden abgerufen …</string>
<string name="express_fetch_best_rates">Aktuelle Preise werden abgerufen ...</string>
<string name="express_floating_rate">Variabler Zinssatz</string>
<string name="express_legal_one_placeholder">Durch die Nutzung der Swap-Funktion erklärst du dich mit den folgenden Bedingungen des Anbieters einverstanden %s</string>
<string name="express_legal_two_placeholders">Durch die Nutzung der Swap-Funktionalität erklärst du dich mit des Anbieters %1$s und %2$s einverstanden</string>
<string name="express_more_providers_soon">Weitere Anbieter werden bald folgen.\nBleib dabei!</string>
<string name="express_more_providers_soon">Weitere Anbieter folgen in Kürze</string>
<string name="express_provider">Anbieter</string>
<string name="express_provider_best_rate">Bester Preis</string>
<string name="express_provider_max_amount">Verfügbar bis zu %s</string>
@ -419,6 +419,7 @@
<string name="markets_add_to_my_portfolio_unavailable_description">Dieses Asset wird derzeit in der Wallet nicht unterstützt.</string>
<string name="markets_add_to_my_portfolio_unavailable_for_wallet_description">Dieses Asset ist für dieses Wallet nicht verfügbar</string>
<string name="markets_add_token">Hinzufügen</string>
<string name="markets_apy_placeholder">APY %s</string>
<string name="markets_available_networks">Verfügbare Netzwerke</string>
<string name="markets_common_my_portfolio">Mein Portfolio</string>
<string name="markets_common_title">Markt</string>
@ -448,6 +449,9 @@
<string name="markets_sort_by_top_gainers_title">Top-Gewinner</string>
<string name="markets_sort_by_top_losers_title">Top-Verlierer</string>
<string name="markets_sort_by_trending_title">Beliebt</string>
<string name="markets_staking_banner_description_placeholder">Staking ist der einfachste Weg, um Belohnungen für Deine Kryptowährung zu erhalten. %s</string>
<string name="markets_staking_banner_description_show_more">Mehr anzeigen</string>
<string name="markets_staking_banner_title">Verdiene bis zu %s APY</string>
<string name="markets_token_details_about_token_title">Über %s</string>
<plurals name="markets_token_details_amount_exchanges">
<item quantity="one">%d börse</item>
@ -530,6 +534,13 @@
<string name="nft_details_base_information">Basisinformationen</string>
<string name="nft_details_chain">Netzwerk</string>
<string name="nft_details_contract_address">Vertragsadresse</string>
<string name="nft_details_info_chain">Kontrollabschnitt</string>
<string name="nft_details_info_contract_address">Kontrollabschnitt</string>
<string name="nft_details_info_rarity_label">Kontrollabschnitt</string>
<string name="nft_details_info_rarity_rank">Kontrollabschnitt</string>
<string name="nft_details_info_token_address">Kontrollabschnitt</string>
<string name="nft_details_info_token_id">Kontrollabschnitt</string>
<string name="nft_details_info_token_standard">Kontrollabschnitt</string>
<string name="nft_details_last_sale_price">Letzter Verkaufspreis</string>
<string name="nft_details_rarity_label">Raritätsetikett</string>
<string name="nft_details_rarity_rank">Seltenheitsgrad</string>
@ -1051,6 +1062,8 @@
<string name="transaction_history_transaction_validator">Validierer: %s</string>
<string name="transfer_min_amount_error">Minimum %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">Der Mindesttransaktionsbetrag beträgt %1$s.</string>
<string name="tron_will_be_send_token_fee_description">Die Netzwerkgebühren für beliebte Token im Tron-Netzwerk können höher sein. Das Staking von TRX kann helfen, die Transaktionskosten zu senken.</string>
<string name="tron_will_be_send_token_fee_title">Spare bei den Tron-Netzwerkgebühren</string>
<string name="try_to_load_data_again_button_title">Versuche es erneut</string>
<string name="twin_error_same_card">Du hast dieselbe Karte oder Ring gescannt. Um ein Zwillings-Wallet zu erstellen, musst du die Karte oder Ring mit der Nummer %d scannen.</string>
<string name="twin_error_wrong_twin">Du hast die falsche Doppelkarte oder Ring gescannt. Bitte versuche eine andere Karte oder Ring</string>
@ -1237,6 +1250,7 @@
<string name="wc_alert_connection_timeout_description">Zeitüberschreitungsfehler. Bitte versuche es später erneut.</string>
<string name="wc_alert_connection_timeout_title">WalletConnect konnte nicht hergestellt werden</string>
<string name="wc_alert_domain_issues_description">Diese Domäne kann nicht verifiziert werden. Überprüfe die Anfrage sorgfältig und bestätige diese dann.</string>
<string name="wc_alert_session_disconnected_description">Geh zurück zu Deinem Browser und stell erneut eine Verbindung über WalletConnect her.</string>
<string name="wc_alert_session_disconnected_title">WalletConnect-Sitzung wurde getrennt</string>
<string name="wc_alert_unknown_error_description">Fehlercode: %s. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support.</string>
<string name="wc_alert_unknown_error_title">Wir haben einen unbekannten Fehler festgestellt.</string>
@ -1244,14 +1258,22 @@
<string name="wc_alert_unsupported_networks_title">Nicht unterstützte Netzwerke</string>
<string name="wc_alert_verified_domain_description">Tangem unterstützt ein erforderliches Netzwerk um %s.</string>
<string name="wc_alert_verified_domain_title">Verifizierte Domain</string>
<string name="wc_alert_wrong_card_description">Falsche Karte oder falscher Ring in der Tangem-App ausgewählt</string>
<string name="wc_alert_wrong_card_title">Wir haben eine Art Problem</string>
<string name="wc_common_connect">Verbinden</string>
<string name="wc_common_network">Netzwerk</string>
<string name="wc_common_networks">Netzwerke</string>
<string name="wc_common_wallet">Wallet</string>
<string name="wc_connected_networks">Verbundene Netzwerke</string>
<string name="wc_connection_reqeust_can_view_balance">Anzeige des Kontostands und der Aktivitäten in Deiner Wallet</string>
<string name="wc_connection_reqeust_cant_sign">Signiere die Transaktionen ohne eine Vorankündigung</string>
<string name="wc_connection_reqeust_request_approval">Genehmigung für Transaktionen anfordern</string>
<string name="wc_connection_reqeust_will_not">Wird nicht in der Lage sein,</string>
<string name="wc_connection_reqeust_would_like">Möchte</string>
<string name="wc_connection_request">Verbindungsanfrage</string>
<string name="wc_connections">Verbindungen</string>
<string name="wc_contents">Inhalt</string>
<string name="wc_copy_data_button_text">Daten kopieren</string>
<string name="wc_disconnect_all">Alle trennen</string>
<string name="wc_disconnect_all_alert_desc">Text über die Trennung aller dApps</string>
<string name="wc_disconnect_all_alert_title">Alle dApps trennen</string>
@ -1262,6 +1284,11 @@
<string name="wc_no_sessions_title">Keine Sitzungen</string>
<string name="wc_notification_security_risk_subtitle">Diese Domain wird von mehreren Sicherheitsanbietern als unsicher eingestuft. Verlasse diese umgehend, um Dein Vermögen zu schützen.</string>
<string name="wc_notification_security_risk_title">Bekanntes Sicherheitsrisiko</string>
<string name="wc_request_from">Anfrage von</string>
<string name="wc_signature_type">Art der Signatur</string>
<string name="wc_transaction_request">Transaktionsanfrage</string>
<string name="wc_transaction_request_title">Transaktionsanfrage</string>
<string name="wc_wallet_connect">Wallet verbinden</string>
<string name="welcome_interrupted_backup_alert_discard">Verwerfen</string>
<string name="welcome_interrupted_backup_alert_message">Du hast eine unterbrochene Sicherung. Möchtest du diese fortsetzen?</string>
<string name="welcome_interrupted_backup_alert_resume">Ja, fortsetzen</string>

View file

@ -527,6 +527,13 @@
<string name="nft_details_base_information">基本情報</string>
<string name="nft_details_chain">チェーン</string>
<string name="nft_details_contract_address">コントラクトアドレス</string>
<string name="nft_details_info_chain">スタブ</string>
<string name="nft_details_info_contract_address">スタブ</string>
<string name="nft_details_info_rarity_label">スタブ</string>
<string name="nft_details_info_rarity_rank">スタブ</string>
<string name="nft_details_info_token_address">スタブ</string>
<string name="nft_details_info_token_id">スタブ</string>
<string name="nft_details_info_token_standard">スタブ</string>
<string name="nft_details_last_sale_price">最終販売価格</string>
<string name="nft_details_rarity_label">レアリティ・ラベル</string>
<string name="nft_details_rarity_rank">レアリティ・ランク</string>
@ -1044,7 +1051,7 @@
<string name="transaction_history_transaction_validator">バリデーター: %s</string>
<string name="transfer_min_amount_error">最小%s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">最小取引金額は%1$sです。</string>
<string name="tron_will_be_send_token_fee_description">Tronネットワークの人気トークンの手数料は高めです。TRXをステーキングすると、より安く、あるいは無料で取引できます。</string>
<string name="tron_will_be_send_token_fee_description">Tronネットワークの人気トークンの手数料は高い可能性があります。TRXをステーキングすると、取引コストを削減できる可能性があります。</string>
<string name="tron_will_be_send_token_fee_title">Tronネットワーク手数料を節約</string>
<string name="try_to_load_data_again_button_title">もう一度やり直してください</string>
<string name="twin_error_same_card">同じカードをスキャンしました。ツインウォレットを作成するには、番号%dのカードをスキャンする必要があります。</string>
@ -1237,6 +1244,7 @@
<string name="wc_alert_verified_domain_title">検証済みドメイン</string>
<string name="wc_alert_wrong_card_description">Tangemアプリで誤ったカードまたはリングが選択されました</string>
<string name="wc_alert_wrong_card_title">問題が起きています</string>
<string name="wc_common_address">アドレス</string>
<string name="wc_common_connect">接続する</string>
<string name="wc_common_network">ネットワーク</string>
<string name="wc_common_networks">ネットワーク</string>
@ -1249,6 +1257,8 @@
<string name="wc_connection_reqeust_would_like">したい</string>
<string name="wc_connection_request">接続リクエスト</string>
<string name="wc_connections">接続</string>
<string name="wc_contents">内容</string>
<string name="wc_copy_data_button_text">データをコピー</string>
<string name="wc_disconnect_all">すべての接続を解除する</string>
<string name="wc_disconnect_all_alert_desc">すべてのdAppsの接続解除に関するテキスト</string>
<string name="wc_disconnect_all_alert_title">すべてのdAppを接続解除する</string>
@ -1259,6 +1269,11 @@
<string name="wc_no_sessions_title">セッションなし</string>
<string name="wc_notification_security_risk_subtitle">このドメインは複数のセキュリティプロバイダーから安全でないとの警告を受けています。あなたの資産を守るため、直ちにアクセスを中止してください。</string>
<string name="wc_notification_security_risk_title">既知のセキュリティリスク</string>
<string name="wc_request_from">リクエスト元</string>
<string name="wc_signature_type">署名タイプ</string>
<string name="wc_transaction_info_to_title">宛先</string>
<string name="wc_transaction_request">取引リクエスト</string>
<string name="wc_transaction_request_title">取引リクエスト</string>
<string name="wc_wallet_connect">ウォレットコネクト</string>
<string name="welcome_interrupted_backup_alert_discard">破棄</string>
<string name="welcome_interrupted_backup_alert_message">バックアップが中断されました。再開しますか?</string>

View file

@ -1063,7 +1063,7 @@
<string name="transaction_history_transaction_validator">validator: %s</string>
<string name="transfer_min_amount_error">Minimum %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">The minimum transaction amount is %1$s.</string>
<string name="tron_will_be_send_token_fee_description">Tron network fees for popular tokens are higher. Stake some TRX for cheaper or free transactions.</string>
<string name="tron_will_be_send_token_fee_description">Tron network fees for popular tokens can be higher. Staking TRX may help reduce transaction costs.</string>
<string name="tron_will_be_send_token_fee_title">Save on Tron network fees</string>
<string name="try_to_load_data_again_button_title">Try again</string>
<string name="twin_error_same_card">You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d</string>
@ -1305,6 +1305,7 @@
<string name="wc_alert_verified_domain_title">Verified domain</string>
<string name="wc_alert_wrong_card_description">Wrong card or ring selected in Tangem App</string>
<string name="wc_alert_wrong_card_title">We\'ve got some kind of problem</string>
<string name="wc_common_address">Address</string>
<string name="wc_common_connect">Connect</string>
<string name="wc_common_network">Network</string>
<string name="wc_common_networks">Networks</string>
@ -1331,6 +1332,7 @@
<string name="wc_notification_security_risk_title">Known security risk</string>
<string name="wc_request_from">Request from</string>
<string name="wc_signature_type">Signature Type</string>
<string name="wc_transaction_info_to_title">To</string>
<string name="wc_transaction_request">Transaction request</string>
<string name="wc_transaction_request_title">Transaction request</string>
<string name="wc_wallet_connect">Wallet connect</string>

View file

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">
<path
android:pathData="M9,0.547C8.407,0.547 7.867,0.701 7.283,0.956C6.721,1.201 6.073,1.562 5.271,2.008L2.073,3.788C1.611,4.045 1.214,4.374 0.939,4.825C0.662,5.278 0.547,5.793 0.547,6.357L0.547,6.411C0.546,6.602 0.546,6.825 0.6,7.027C0.733,7.53 1.055,7.86 1.448,8.035C1.663,8.13 1.887,8.175 2.088,8.194V13.66C1.167,14.188 0.547,15.181 0.547,16.318C0.547,16.945 1.055,17.453 1.682,17.453H8.23C8.644,17.453 8.98,17.117 8.98,16.703C8.98,16.289 8.644,15.953 8.23,15.953H2.09C2.254,15.267 2.872,14.757 3.608,14.757H8.23C8.644,14.757 8.98,14.421 8.98,14.007C8.98,13.593 8.644,13.257 8.23,13.257H6.669V8.21H15.572C15.834,8.21 16.205,8.19 16.553,8.035C16.946,7.86 17.268,7.53 17.401,7.027C17.455,6.825 17.454,6.602 17.454,6.411L17.454,6.357C17.454,5.793 17.339,5.278 17.062,4.825C16.787,4.374 16.39,4.045 15.928,3.788L12.73,2.008C11.928,1.562 11.28,1.201 10.718,0.956C10.134,0.701 9.594,0.547 9,0.547ZM5.968,3.337C6.81,2.868 7.395,2.543 7.883,2.33C8.356,2.124 8.685,2.047 9,2.047C9.316,2.047 9.645,2.124 10.118,2.33C10.606,2.543 11.191,2.868 12.033,3.337L15.198,5.098C15.506,5.269 15.679,5.437 15.782,5.606C15.883,5.772 15.954,6 15.954,6.357C15.954,6.489 15.954,6.563 15.951,6.617C15.95,6.635 15.95,6.646 15.949,6.652C15.948,6.657 15.946,6.66 15.945,6.663L15.942,6.664C15.923,6.673 15.884,6.686 15.817,6.695C15.75,6.705 15.67,6.71 15.572,6.71H2.429C2.331,6.71 2.251,6.705 2.184,6.695C2.116,6.686 2.078,6.673 2.059,6.664L2.056,6.663C2.055,6.66 2.053,6.657 2.052,6.652C2.051,6.646 2.051,6.635 2.05,6.617C2.047,6.563 2.047,6.489 2.047,6.357C2.047,6 2.118,5.772 2.219,5.606C2.322,5.437 2.495,5.269 2.802,5.098L5.968,3.337ZM3.608,13.257L3.588,13.257V8.21H5.169V13.257H3.608Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M10.396,15.788C10.103,16.081 10.103,16.556 10.396,16.849C10.689,17.142 11.164,17.142 11.457,16.849L16.849,11.456C17.142,11.163 17.142,10.689 16.849,10.396C16.556,10.103 16.081,10.103 15.788,10.396L10.396,15.788Z"
android:fillColor="#000000"/>
<path
android:pathData="M10.176,11.503C10.176,10.77 10.77,10.176 11.504,10.176C12.237,10.176 12.832,10.77 12.832,11.503C12.832,12.237 12.237,12.831 11.504,12.831C10.77,12.831 10.176,12.237 10.176,11.503ZM11.504,11.676C11.599,11.676 11.676,11.599 11.676,11.503C11.676,11.408 11.599,11.331 11.504,11.331C11.409,11.331 11.332,11.408 11.332,11.503C11.332,11.599 11.409,11.676 11.504,11.676Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M15.741,14.412C15.008,14.412 14.413,15.007 14.413,15.74C14.413,16.473 15.008,17.068 15.741,17.068C16.474,17.068 17.069,16.473 17.069,15.74C17.069,15.007 16.474,14.412 15.741,14.412ZM15.913,15.74C15.913,15.835 15.836,15.912 15.741,15.912C15.646,15.912 15.569,15.835 15.569,15.74C15.569,15.645 15.646,15.568 15.741,15.568C15.836,15.568 15.913,15.645 15.913,15.74Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M9.992,4.701C8.321,4.701 6.964,6.054 6.964,7.726C6.964,9.397 8.321,10.75 9.992,10.75C11.663,10.75 13.02,9.397 13.02,7.726C13.02,6.054 11.663,4.701 9.992,4.701ZM8.464,7.726C8.464,6.885 9.147,6.201 9.992,6.201C10.837,6.201 11.52,6.885 11.52,7.726C11.52,8.567 10.837,9.25 9.992,9.25C9.147,9.25 8.464,8.567 8.464,7.726Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M6.448,15.067C8.288,13.14 11.709,13.068 13.542,15.058C13.823,15.362 14.298,15.382 14.602,15.101C14.907,14.82 14.926,14.346 14.645,14.041C12.191,11.377 7.766,11.515 5.363,14.031C5.077,14.331 5.088,14.806 5.387,15.092C5.687,15.378 6.162,15.367 6.448,15.067Z"
android:fillColor="#000000"/>
<path
android:pathData="M10.056,0.606H9.942C7.953,0.606 6.384,0.606 5.157,0.771C3.897,0.941 2.886,1.296 2.091,2.092C1.295,2.887 0.94,3.898 0.77,5.158C0.605,6.385 0.605,7.954 0.605,9.943V10.057C0.605,12.046 0.605,13.615 0.77,14.842C0.94,16.102 1.295,17.113 2.091,17.908C2.886,18.704 3.897,19.059 5.157,19.229C6.384,19.394 7.953,19.394 9.942,19.394H10.056C12.045,19.394 13.614,19.394 14.841,19.229C16.101,19.059 17.112,18.704 17.907,17.908C18.703,17.113 19.058,16.102 19.228,14.842C19.393,13.615 19.393,12.046 19.393,10.057V9.943C19.393,7.954 19.393,6.385 19.228,5.158C19.058,3.898 18.703,2.887 17.907,2.092C17.112,1.296 16.101,0.941 14.841,0.771C13.614,0.606 12.045,0.606 10.056,0.606ZM3.152,3.153C3.622,2.682 4.263,2.405 5.357,2.258C6.472,2.108 7.941,2.106 9.999,2.106C12.057,2.106 13.526,2.108 14.641,2.258C15.735,2.405 16.376,2.682 16.846,3.153C17.317,3.623 17.594,4.264 17.741,5.358C17.891,6.473 17.893,7.941 17.893,10C17.893,12.059 17.891,13.527 17.741,14.642C17.594,15.736 17.317,16.377 16.846,16.847C16.376,17.318 15.735,17.595 14.641,17.742C13.526,17.892 12.057,17.894 9.999,17.894C7.941,17.894 6.472,17.892 5.357,17.742C4.263,17.595 3.622,17.318 3.152,16.847C2.681,16.377 2.404,15.736 2.257,14.642C2.107,13.527 2.105,12.059 2.105,10C2.105,7.941 2.107,6.473 2.257,5.358C2.404,4.264 2.681,3.623 3.152,3.153Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>

View file

@ -14,9 +14,11 @@ dependencies {
implementation(projects.core.datasource)
/* Domain */
implementation(projects.domain.models)
implementation(projects.domain.demo)
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
/* Libs - SDK */
implementation(tangemDeps.blockchain)
@ -28,8 +30,16 @@ dependencies {
kapt(deps.hilt.kapt)
/* Libs - Other */
implementation(deps.kotlin.coroutines)
implementation(deps.jodatime)
implementation(deps.timber)
implementation(deps.androidx.datastore)
implementation(deps.arrow.core)
implementation(deps.jodatime)
implementation(deps.kotlin.coroutines)
implementation(deps.timber)
/* Test */
testImplementation(projects.common.test)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
}

View file

@ -0,0 +1,46 @@
package com.tangem.data.common.currency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
/**
* Factory for creating list of [CryptoCurrency] for selected card
*
[REDACTED_AUTHOR]
*/
interface CardCryptoCurrencyFactory {
/**
* Universal method for creating list of [CryptoCurrency] in [network] for any card
*
* @param userWalletId user wallet id that determines type of card
* @param network network
*/
@Throws
suspend fun create(userWalletId: UserWalletId, network: Network): List<CryptoCurrency>
/**
* Create default coins for multi currency card
*
* @param scanResponse scan response
*/
fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List<CryptoCurrency.Coin>
/**
* Create primary currency for single currency card
*
* @param scanResponse scan response
*/
@Throws
fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency
/**
* Create currencies for single currency card with token (like, NODL)
*
* @param scanResponse scan response
*/
@Throws
fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List<CryptoCurrency>
}

View file

@ -0,0 +1,128 @@
package com.tangem.data.common.currency
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
/**
* Default implementation of factory for creating list of [CryptoCurrency] for selected card
*
* @property demoConfig demo config
* @property excludedBlockchains excluded blockchains
* @property userWalletsStore user wallets store
* @property userTokensResponseStore user tokens response store
*/
internal class DefaultCardCryptoCurrencyFactory(
private val demoConfig: DemoConfig,
private val excludedBlockchains: ExcludedBlockchains,
private val userWalletsStore: UserWalletsStore,
private val userTokensResponseStore: UserTokensResponseStore,
) : CardCryptoCurrencyFactory {
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory(excludedBlockchains) }
override suspend fun create(userWalletId: UserWalletId, network: Network): List<CryptoCurrency> {
val userWallet = userWalletsStore.getSyncStrict(key = userWalletId)
val blockchain = Blockchain.fromNetworkId(networkId = network.backendId)
// multi-currency wallet
if (userWallet.isMultiCurrency) return getMultiWalletCurrencies(userWallet = userWallet, network = network)
// check if the blockchain of single-currency wallet is the same as network
val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain()
if (cardBlockchain != blockchain) return emptyList()
// single-currency wallet with token (NODL)
if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
return createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
}
// single-currency wallet
return createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse).let(::listOf)
}
override fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List<CryptoCurrency.Coin> {
val card = scanResponse.card
var blockchains = if (demoConfig.isDemoCardId(card.cardId)) {
demoConfig.demoBlockchains
} else {
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}
if (card.isTestCard) {
blockchains = blockchains.mapNotNull { it.getTestnetVersion() }
}
return blockchains.mapNotNull {
cryptoCurrencyFactory.createCoin(
blockchain = it,
extraDerivationPath = null,
scanResponse = scanResponse,
)
}
}
override fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency {
return with(getSingleWalletCurrencies(scanResponse)) {
primaryToken ?: coin
}
}
override fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List<CryptoCurrency> {
return with(getSingleWalletCurrencies(scanResponse)) {
listOfNotNull(coin, primaryToken)
}
}
private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, network: Network): List<CryptoCurrency> {
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
?: return emptyList()
val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains)
return responseCurrenciesFactory.createCurrencies(
tokens = response.tokens.filter {
it.networkId == network.backendId && it.derivationPath == network.derivationPath.value
},
scanResponse = userWallet.scanResponse,
)
}
private fun getSingleWalletCurrencies(scanResponse: ScanResponse): SingleWalletCurrencies {
val resolver = scanResponse.cardTypesResolver
val blockchain = resolver.getBlockchain()
val coin = cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
)
requireNotNull(coin) { "Coin for the single currency card cannot be null" }
val primaryToken = resolver.getPrimaryToken()?.let { token ->
cryptoCurrencyFactory.createToken(
sdkToken = token,
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
)
}
return SingleWalletCurrencies(coin = coin, primaryToken = primaryToken)
}
private data class SingleWalletCurrencies(val coin: CryptoCurrency, val primaryToken: CryptoCurrency?)
}

View file

@ -0,0 +1,33 @@
package com.tangem.data.common.di
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.DefaultCardCryptoCurrencyFactory
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.demo.DemoConfig
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 DataCommonModule {
@Provides
@Singleton
fun provideCardCryptoCurrencyFactory(
excludedBlockchains: ExcludedBlockchains,
userWalletsStore: UserWalletsStore,
userTokensResponseStore: UserTokensResponseStore,
): CardCryptoCurrencyFactory {
return DefaultCardCryptoCurrencyFactory(
demoConfig = DemoConfig(),
excludedBlockchains = excludedBlockchains,
userWalletsStore = userWalletsStore,
userTokensResponseStore = userTokensResponseStore,
)
}
}

View file

@ -0,0 +1,430 @@
package com.tangem.data.common.currency
import android.net.Uri
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.card.WalletData
import com.tangem.common.test.domain.card.MockScanResponseFactory
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.configs.GenericCardConfig
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
/**
[REDACTED_AUTHOR]
*/
internal class DefaultCardCryptoCurrencyFactoryTest {
private val userWalletsStore: UserWalletsStore = mockk()
private val userTokensResponseStore: UserTokensResponseStore = mockk()
private val factory = DefaultCardCryptoCurrencyFactory(
demoConfig = DemoConfig(),
excludedBlockchains = ExcludedBlockchains(),
userWalletsStore = userWalletsStore,
userTokensResponseStore = userTokensResponseStore,
)
@Before
fun setup() {
mockkStatic(Uri::class)
every { Uri.parse(any()) } returns mockk()
}
@Test
fun `test create if userTokensResponse is not empty`() = runTest {
val multiWallet = createMultiWallet()
val userTokensResponse = UserTokensResponseFactory().createUserTokensResponse(
currencies = listOf(ethereum),
isGroupedByNetwork = false,
isSortedByBalance = false,
)
coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet
coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns userTokensResponse
val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network)
coVerifyOrder {
userWalletsStore.getSyncStrict(key = multiWallet.walletId)
userTokensResponseStore.getSyncOrNull(multiWallet.walletId)
}
val expected = listOf(ethereum)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test create if userTokensResponse is empty`() = runTest {
val multiWallet = createMultiWallet()
val userTokensResponse = UserTokensResponseFactory().createUserTokensResponse(
currencies = listOf(),
isGroupedByNetwork = false,
isSortedByBalance = false,
)
coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet
coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns userTokensResponse
val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network)
coVerifyOrder {
userWalletsStore.getSyncStrict(key = multiWallet.walletId)
userTokensResponseStore.getSyncOrNull(multiWallet.walletId)
}
val expected = emptyList<CryptoCurrency>()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test create if userTokensResponse is null`() = runTest {
val multiWallet = createMultiWallet()
coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet
coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns null
val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network)
coVerifyOrder {
userWalletsStore.getSyncStrict(key = multiWallet.walletId)
userTokensResponseStore.getSyncOrNull(multiWallet.walletId)
}
val expected = emptyList<CryptoCurrency>()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test create if userTokensResponse does not contain currency of selected network`() = runTest {
val multiWallet = createMultiWallet()
val userTokensResponse = UserTokensResponseFactory().createUserTokensResponse(
currencies = listOf(bitcoin),
isGroupedByNetwork = false,
isSortedByBalance = false,
)
coEvery { userWalletsStore.getSyncStrict(key = multiWallet.walletId) } returns multiWallet
coEvery { userTokensResponseStore.getSyncOrNull(multiWallet.walletId) } returns userTokensResponse
val actual = factory.create(userWalletId = multiWallet.walletId, network = ethereum.network)
coVerifyOrder {
userWalletsStore.getSyncStrict(key = multiWallet.walletId)
userTokensResponseStore.getSyncOrNull(multiWallet.walletId)
}
val expected = emptyList<CryptoCurrency>()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test create if single wallet has another primary network`() = runTest {
val singleWallet = createSingleWallet()
coEvery { userWalletsStore.getSyncStrict(key = singleWallet.walletId) } returns singleWallet
val actual = factory.create(userWalletId = singleWallet.walletId, network = bitcoin.network)
coVerifyOrder {
userWalletsStore.getSyncStrict(key = singleWallet.walletId)
singleWallet.scanResponse.cardTypesResolver.getBlockchain()
}
val expected = emptyList<CryptoCurrency>()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test create if card is single wallet`() = runTest {
val singleWallet = createSingleWallet()
coEvery { userWalletsStore.getSyncStrict(key = singleWallet.walletId) } returns singleWallet
val actual = factory.create(userWalletId = singleWallet.walletId, network = ethereum.network)
coVerifyOrder {
userWalletsStore.getSyncStrict(key = singleWallet.walletId)
singleWallet.scanResponse.cardTypesResolver.getBlockchain()
}
val expected = listOf(ethereum)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test create if card is single wallet with token`() = runTest {
val singleWallet = createSingleWalletWithToken()
coEvery { userWalletsStore.getSyncStrict(key = singleWallet.walletId) } returns singleWallet
val actual = factory.create(userWalletId = singleWallet.walletId, network = ethereum.network)
val token = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()).createToken(
sdkToken = singleWallet.scanResponse.cardTypesResolver.getPrimaryToken()!!,
blockchain = Blockchain.Ethereum,
extraDerivationPath = null,
scanResponse = singleWallet.scanResponse,
)
val expected = listOf(ethereum, token)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test createDefaultCoinsForMultiCurrencyCard if card is prod`() = runTest {
val multiWallet = createMultiWallet()
val actual = factory.createDefaultCoinsForMultiCurrencyCard(scanResponse = multiWallet.scanResponse)
val expected = listOf(bitcoin, ethereum)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test createDefaultCoinsForMultiCurrencyCard if card is test`() = runTest {
val multiWallet = createMultiWallet().let {
it.copy(
scanResponse = it.scanResponse.copy(
card = it.scanResponse.card.copy(cardId = "FF99", batchId = "99FF"),
),
)
}
val actual = factory.createDefaultCoinsForMultiCurrencyCard(scanResponse = multiWallet.scanResponse)
val expected = listOf(
cryptoCurrencyFactory.createCoin(blockchain = Blockchain.BitcoinTestnet),
cryptoCurrencyFactory.createCoin(blockchain = Blockchain.EthereumTestnet).setCanHandleTokens(true),
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test createDefaultCoinsForMultiCurrencyCard if card is demo`() = runTest {
val multiWallet = createMultiWallet().let {
it.copy(
scanResponse = it.scanResponse.copy(
card = it.scanResponse.card.copy(cardId = "AC01000000041225"),
),
)
}
val actual = factory.createDefaultCoinsForMultiCurrencyCard(scanResponse = multiWallet.scanResponse)
val expected = listOf(
bitcoin,
ethereum,
cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Dogecoin),
cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Solana),
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test createPrimaryCurrencyForSingleCurrencyCard if unable to create token`() = runTest {
val singleWallet = UserWallet(
name = "Note",
walletId = UserWalletId("011"),
cardsInWallet = setOf(),
isMultiCurrency = false,
scanResponse = MockScanResponseFactory.create(
cardConfig = GenericCardConfig(maxWalletCount = 2),
derivedKeys = emptyMap(),
),
hasBackupError = false,
)
val actual = runCatching {
factory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = singleWallet.scanResponse)
}
val exception = IllegalArgumentException("Coin for the single currency card cannot be null")
Truth.assertThat(actual.isFailure).isTrue()
Truth.assertThat(actual.exceptionOrNull()).isInstanceOf(exception::class.java)
Truth.assertThat(actual.exceptionOrNull()).hasMessageThat().isEqualTo(exception.message)
}
@Test
fun `test createPrimaryCurrencyForSingleCurrencyCard if primaryToken is null`() = runTest {
val singleWallet = createSingleWallet()
val actual = factory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = singleWallet.scanResponse)
val expected = ethereum
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test createPrimaryCurrencyForSingleCurrencyCard if primaryToken is not null`() = runTest {
val singleWallet = createSingleWalletWithToken()
val actual = factory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = singleWallet.scanResponse)
val expected = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()).createToken(
sdkToken = singleWallet.scanResponse.cardTypesResolver.getPrimaryToken()!!,
blockchain = Blockchain.Ethereum,
extraDerivationPath = null,
scanResponse = singleWallet.scanResponse,
)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test createCurrenciesForSingleCurrencyCardWithToken if unable to create token`() = runTest {
val singleWalletWithToken = UserWallet(
name = "Note",
walletId = UserWalletId("011"),
cardsInWallet = setOf(),
isMultiCurrency = false,
scanResponse = MockScanResponseFactory.create(
cardConfig = GenericCardConfig(maxWalletCount = 2),
derivedKeys = emptyMap(),
),
hasBackupError = false,
)
val actual = runCatching {
factory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = singleWalletWithToken.scanResponse)
}
val exception = IllegalArgumentException("Coin for the single currency card cannot be null")
Truth.assertThat(actual.isFailure).isTrue()
Truth.assertThat(actual.exceptionOrNull()).isInstanceOf(exception::class.java)
Truth.assertThat(actual.exceptionOrNull()).hasMessageThat().isEqualTo(exception.message)
}
@Test
fun `test createCurrenciesForSingleCurrencyCardWithToken if primaryToken is null`() = runTest {
val singleWalletWithToken = createSingleWallet()
val actual = factory.createCurrenciesForSingleCurrencyCardWithToken(
scanResponse = singleWalletWithToken.scanResponse,
)
val expected = listOf(ethereum)
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `test createCurrenciesForSingleCurrencyCardWithToken if primaryToken is not null`() = runTest {
val singleWalletWithToken = createSingleWalletWithToken()
val actual = factory.createCurrenciesForSingleCurrencyCardWithToken(
scanResponse = singleWalletWithToken.scanResponse,
)
val token = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()).createToken(
sdkToken = singleWalletWithToken.scanResponse.cardTypesResolver.getPrimaryToken()!!,
blockchain = Blockchain.Ethereum,
extraDerivationPath = null,
scanResponse = singleWalletWithToken.scanResponse,
)
val expected = listOf(ethereum, token)
Truth.assertThat(actual).isEqualTo(expected)
}
private fun createMultiWallet(): UserWallet {
return UserWallet(
name = "Wallet 1",
walletId = UserWalletId("011"),
cardsInWallet = setOf(),
isMultiCurrency = true,
scanResponse = MockScanResponseFactory.create(
cardConfig = GenericCardConfig(maxWalletCount = 2),
derivedKeys = emptyMap(),
),
hasBackupError = false,
)
}
private fun createSingleWallet(): UserWallet {
return UserWallet(
name = "Note",
walletId = UserWalletId("011"),
cardsInWallet = setOf(),
isMultiCurrency = false,
scanResponse = MockScanResponseFactory.create(
cardConfig = GenericCardConfig(maxWalletCount = 2),
derivedKeys = emptyMap(),
).let {
it.copy(
card = it.card.copy(batchId = "AB10"),
productType = ProductType.Note,
)
},
hasBackupError = false,
)
}
private fun createSingleWalletWithToken(): UserWallet {
return UserWallet(
name = "NODL",
walletId = UserWalletId("011"),
cardsInWallet = setOf(),
isMultiCurrency = false,
scanResponse = MockScanResponseFactory.create(
cardConfig = GenericCardConfig(maxWalletCount = 2),
derivedKeys = emptyMap(),
).copy(
productType = ProductType.Note,
walletData = WalletData(
blockchain = "ETH",
token = WalletData.Token(
name = "Ethereum",
symbol = "ETH",
contractAddress = "0x",
decimals = 8,
),
),
),
hasBackupError = false,
)
}
private companion object {
val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
val ethereum = cryptoCurrencyFactory.ethereum.setCanHandleTokens(value = true)
val bitcoin = cryptoCurrencyFactory.createCoin(blockchain = Blockchain.Bitcoin)
fun CryptoCurrency.setCanHandleTokens(value: Boolean): CryptoCurrency {
return when (this) {
is CryptoCurrency.Coin -> copy(network = network.copy(canHandleTokens = value))
is CryptoCurrency.Token -> copy(network = network.copy(canHandleTokens = value))
}
}
}
}

View file

@ -6,12 +6,12 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.UserTokensResponseFactory
import com.tangem.data.common.currency.getBlockchain
import com.tangem.data.common.utils.retryOnError
import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher
import com.tangem.data.managetokens.utils.ManagedCryptoCurrencyFactory
import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
@ -26,7 +26,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.demo.DemoConfig
import com.tangem.domain.managetokens.model.*
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
import com.tangem.domain.managetokens.repository.ManageTokensRepository
@ -48,12 +47,12 @@ internal class DefaultManageTokensRepository(
private val appPreferencesStore: AppPreferencesStore,
private val testnetTokensStorage: TestnetTokensStorage,
private val excludedBlockchains: ExcludedBlockchains,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
private val dispatchers: CoroutineDispatcherProvider,
) : ManageTokensRepository {
private val managedCryptoCurrencyFactory = ManagedCryptoCurrencyFactory(excludedBlockchains)
private val userTokensResponseFactory = UserTokensResponseFactory()
private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(DemoConfig(), excludedBlockchains)
// region getTokenListBatchFlow
override fun getTokenListBatchFlow(
@ -77,7 +76,7 @@ internal class DefaultManageTokensRepository(
prefetchDistance = batchSize,
batchSize = batchSize,
subFetcher = { request, _, isFirstBatchFetching ->
val userWallet = request.params.userWalletId?.let { getUserWallet(it) }
val userWallet = request.params.userWalletId?.let(userWalletsStore::getSyncStrict)
if (userWallet?.scanResponse?.card?.isTestCard == true) {
fetchTestnetCurrencies(userWallet, request)
@ -190,17 +189,11 @@ internal class DefaultManageTokensRepository(
private fun createDefaultUserTokensResponse(userWallet: UserWallet) =
userTokensResponseFactory.createUserTokensResponse(
currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse),
currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse),
isGroupedByNetwork = false,
isSortedByBalance = false,
)
private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet {
return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Unable to find a user wallet with provided ID: $userWalletId"
}
}
private fun getSupportedBlockchains(userWallet: UserWallet?): List<Blockchain> {
return userWallet?.scanResponse?.let {
it.card.supportedBlockchains(it.cardTypesResolver, excludedBlockchains)
@ -261,7 +254,7 @@ internal class DefaultManageTokensRepository(
userWalletId: UserWalletId,
sourceNetwork: SourceNetwork,
): CurrencyUnsupportedState? {
val userWallet = getUserWallet(userWalletId = userWalletId)
val userWallet = userWalletsStore.getSyncStrict(key = userWalletId)
val blockchain = getBlockchain(sourceNetwork.id)
return when (sourceNetwork) {
is SourceNetwork.Default -> checkTokenUnsupportedState(userWallet = userWallet, blockchain = blockchain)
@ -274,7 +267,7 @@ internal class DefaultManageTokensRepository(
rawNetworkId: String,
isMainNetwork: Boolean,
): CurrencyUnsupportedState? {
val userWallet = getUserWallet(userWalletId = userWalletId)
val userWallet = userWalletsStore.getSyncStrict(key = userWalletId)
val blockchain = Blockchain.fromNetworkId(networkId = rawNetworkId)
?: error("Can not create blockchain with given networkId -> $rawNetworkId")
return if (isMainNetwork) {

View file

@ -1,6 +1,7 @@
package com.tangem.data.managetokens.di
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.managetokens.DefaultCustomTokensRepository
import com.tangem.data.managetokens.DefaultManageTokensRepository
import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher
@ -32,15 +33,17 @@ internal object ManageTokensDataModule {
testnetTokensStorage: TestnetTokensStorage,
dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
): ManageTokensRepository {
return DefaultManageTokensRepository(
tangemTechApi,
userWalletsStore,
manageTokensUpdateFetcher,
appPreferencesStore,
testnetTokensStorage,
excludedBlockchains,
dispatchers,
tangemTechApi = tangemTechApi,
userWalletsStore = userWalletsStore,
manageTokensUpdateFetcher = manageTokensUpdateFetcher,
appPreferencesStore = appPreferencesStore,
testnetTokensStorage = testnetTokensStorage,
excludedBlockchains = excludedBlockchains,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
dispatchers = dispatchers,
)
}

View file

@ -1,27 +1,14 @@
package com.tangem.data.networks.single
import arrow.core.Either
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.networks.store.NetworksStatusesStoreV2
import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory
import com.tangem.data.tokens.utils.NetworkStatusFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
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.userwallet.UserWalletsStore
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.core.utils.catchOn
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import timber.log.Timber
@ -30,27 +17,20 @@ import javax.inject.Inject
/**
* Default implementation of [SingleNetworkStatusFetcher]
*
* @param excludedBlockchains excluded blockchains
* @property walletManagersFacade wallet managers facade
* @property networksStatusesStore networks statuses store
* @property userWalletsStore user wallets store
* @property appPreferencesStore app preferences store
* @property dispatchers dispatchers
* @property walletManagersFacade wallet managers facade
* @property networksStatusesStore networks statuses store
* @property cardCryptoCurrencyFactory card crypto currency factory
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
*/
internal class DefaultSingleNetworkStatusFetcher @Inject constructor(
excludedBlockchains: ExcludedBlockchains,
private val walletManagersFacade: WalletManagersFacade,
private val networksStatusesStore: NetworksStatusesStoreV2,
private val userWalletsStore: UserWalletsStore,
private val appPreferencesStore: AppPreferencesStore,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
private val dispatchers: CoroutineDispatcherProvider,
) : SingleNetworkStatusFetcher {
private val demoConfig = DemoConfig()
private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig, excludedBlockchains)
private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains)
private val networkStatusFactory = NetworkStatusFactory()
override suspend fun invoke(params: SingleNetworkStatusFetcher.Params) = Either.catchOn(dispatchers.default) {
@ -59,9 +39,10 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor(
is SingleNetworkStatusFetcher.Params.Simple -> {
networksStatusesStore.refresh(userWalletId = params.userWalletId, network = params.network)
val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId)
createCurrencies(userWallet = userWallet, network = params.network)
cardCryptoCurrencyFactory.create(
userWalletId = params.userWalletId,
network = params.network,
)
}
}
@ -105,36 +86,4 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor(
Timber.e("Failed to fetch network status for $params: $it")
networksStatusesStore.storeError(userWalletId = params.userWalletId, network = params.network)
}
private suspend fun createCurrencies(userWallet: UserWallet, network: Network): List<CryptoCurrency> {
val blockchain = Blockchain.fromNetworkId(networkId = network.backendId)
// multi-currency wallet
if (userWallet.isMultiCurrency) return getMultiWalletCurrencies(userWallet = userWallet, network = network)
// check if the blockchain of single-currency wallet is the same as network
val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain()
if (cardBlockchain != blockchain) return emptyList()
// single-currency wallet with token (NODL)
if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
return cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
}
// single-currency wallet
return cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse).let(::listOf)
}
private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, network: Network): List<CryptoCurrency> {
val response = appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWallet.walletId.stringValue),
) ?: return emptyList()
return responseCurrenciesFactory.createCurrencies(
tokens = response.tokens.filter {
it.networkId == network.backendId && it.derivationPath == network.derivationPath.value
},
scanResponse = userWallet.scanResponse,
)
}
}

View file

@ -2,8 +2,8 @@ package com.tangem.data.networks.single
import com.google.common.truth.Truth
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.networks.store.NetworksStatusesStoreV2
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -22,35 +22,35 @@ import org.junit.Test
*/
internal class DefaultSingleNetworkStatusFetcherTest {
private val walletManagersFacade: WalletManagersFacade = mockk(relaxed = true)
private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxed = true)
private val userWalletsStore: UserWalletsStore = mockk(relaxed = true)
private val walletManagersFacade: WalletManagersFacade = mockk(relaxUnitFun = true)
private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxUnitFun = true)
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk()
private val fetcher = DefaultSingleNetworkStatusFetcher(
excludedBlockchains = mockk(relaxed = true),
walletManagersFacade = walletManagersFacade,
networksStatusesStore = networksStatusesStore,
userWalletsStore = userWalletsStore,
appPreferencesStore = mockk(relaxed = true),
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@Test
fun `fetch network status successfully`() = runTest {
val params = SingleNetworkStatusFetcher.Params.Simple(userWalletId = userWalletId, network = network)
val params = createParams()
coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } returns listOf(ethereum)
val result = UpdateWalletManagerResult.MissedDerivation
coEvery { walletManagersFacade.update(userWalletId, network, emptySet()) } returns result
coEvery { walletManagersFacade.update(params.userWalletId, params.network, emptySet()) } returns result
val actual = fetcher(params)
coVerifyOrder {
networksStatusesStore.refresh(userWalletId = userWalletId, network = network)
userWalletsStore.getSyncStrict(key = userWalletId)
walletManagersFacade.update(userWalletId, network, emptySet())
networksStatusesStore.refresh(params.userWalletId, params.network)
cardCryptoCurrencyFactory.create(params.userWalletId, params.network)
walletManagersFacade.update(params.userWalletId, params.network, emptySet())
networksStatusesStore.storeSuccess(
userWalletId = userWalletId,
value = NetworkStatus(network, NetworkStatus.MissedDerivation),
userWalletId = params.userWalletId,
value = NetworkStatus(params.network, NetworkStatus.MissedDerivation),
)
}
@ -59,17 +59,17 @@ internal class DefaultSingleNetworkStatusFetcherTest {
@Test
fun `fetch network status failure`() = runTest {
val params = SingleNetworkStatusFetcher.Params.Simple(userWalletId = userWalletId, network = network)
val params = createParams()
val exception = IllegalStateException()
coEvery { userWalletsStore.getSyncStrict(key = userWalletId) } throws exception
coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } throws exception
val actual = fetcher(params)
coVerifyOrder {
networksStatusesStore.refresh(userWalletId = userWalletId, network = network)
userWalletsStore.getSyncStrict(key = userWalletId)
networksStatusesStore.storeError(userWalletId = userWalletId, network = network)
networksStatusesStore.refresh(userWalletId = params.userWalletId, network = params.network)
cardCryptoCurrencyFactory.create(userWalletId = params.userWalletId, network = params.network)
networksStatusesStore.storeError(userWalletId = params.userWalletId, network = params.network)
}
coVerify(inverse = true) {
@ -81,8 +81,15 @@ internal class DefaultSingleNetworkStatusFetcherTest {
Truth.assertThat(actual.leftOrNull()).isEqualTo(exception)
}
private fun createParams(): SingleNetworkStatusFetcher.Params {
return SingleNetworkStatusFetcher.Params.Simple(
userWalletId = UserWalletId("011"),
network = ethereum.network,
)
}
private companion object {
val userWalletId = UserWalletId("011")
val network = MockCryptoCurrencyFactory().ethereum.network
val ethereum = MockCryptoCurrencyFactory().ethereum
}
}

View file

@ -4,12 +4,11 @@ import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConv
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.NotificationApplicationCreateBody
import com.tangem.datasource.api.tangemTech.models.WalletBody
import com.tangem.datasource.api.tangemTech.models.WalletIdBody
import com.tangem.utils.info.AppInfoProvider
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.*
import com.tangem.domain.notifications.models.ApplicationId
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.notifications.models.NotificationsEligibleNetwork
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -23,7 +22,7 @@ internal class DefaultNotificationsRepository @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
) : NotificationsRepository {
override suspend fun createApplicationId(pushToken: String?): String = withContext(dispatchers.io) {
override suspend fun createApplicationId(pushToken: String?): ApplicationId = withContext(dispatchers.io) {
tangemTechApi.createApplicationId(
NotificationApplicationCreateBody(
platform = appInfoProvider.platform,
@ -33,15 +32,16 @@ internal class DefaultNotificationsRepository @Inject constructor(
timezone = appInfoProvider.timezone,
pushToken = pushToken,
),
).getOrThrow().appId
).getOrThrow().appId.let(::ApplicationId)
}
override suspend fun saveApplicationId(appId: String) {
appPreferencesStore.store(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY, appId)
override suspend fun saveApplicationId(appId: ApplicationId) {
appPreferencesStore.store(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY, appId.value)
}
override suspend fun getApplicationId(): String? {
override suspend fun getApplicationId(): ApplicationId? {
return appPreferencesStore.getSyncOrNull(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY)
?.let(::ApplicationId)
}
override suspend fun incrementTronTokenFeeNotificationShowCounter() {
@ -61,31 +61,10 @@ internal class DefaultNotificationsRepository @Inject constructor(
)
}
override suspend fun associateApplicationIdWithWallets(appId: String, wallets: List<String>) =
withContext(dispatchers.io) {
tangemTechApi.associateApplicationIdWithWallets(
applicationId = appId,
body = wallets.map {
WalletIdBody(it)
},
).getOrThrow()
}
override suspend fun setWalletName(walletId: String, walletName: String) = withContext(dispatchers.io) {
tangemTechApi.updateWallet(
walletId,
WalletBody(name = walletName),
).getOrThrow()
}
override suspend fun getWalletName(walletId: String): String? = withContext(dispatchers.io) {
tangemTechApi.getWalletById(walletId).getOrThrow().name
}
override suspend fun sendPushToken(appId: String, pushToken: String) {
override suspend fun sendPushToken(appId: ApplicationId, pushToken: String) {
withContext(dispatchers.io) {
tangemTechApi.updatePushTokenForApplicationId(
appId,
appId.value,
NotificationApplicationCreateBody(
pushToken = pushToken,
),

View file

@ -12,6 +12,7 @@ import com.squareup.moshi.Moshi
import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.tangemTech.models.*
import com.tangem.domain.notifications.models.ApplicationId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.coEvery
import io.mockk.coVerify
@ -41,9 +42,9 @@ class DefaultNotificationsRepositoryTest {
fun `GIVEN valid push token WHEN createApplicationId THEN returns application id`() = runTest {
// GIVEN
val pushToken = "test-push-token"
val expectedAppId = "test-app-id"
val expectedAppId = ApplicationId("test-app-id")
val expectedAppIdResponse = NotificationApplicationIdResponse(
appId = expectedAppId,
appId = expectedAppId.value,
)
coEvery { appInfoProvider.platform } returns "android"
coEvery { appInfoProvider.device } returns "test-device"
@ -76,7 +77,7 @@ class DefaultNotificationsRepositoryTest {
@Test
fun `GIVEN application id WHEN saveApplicationId THEN stores it in preferences`() = runTest {
// GIVEN
val appId = "test-app-id"
val appId = ApplicationId("test-app-id")
val preferences = mockk<Preferences>(relaxed = true)
coEvery { preferencesDataStore.updateData(any()) } returns preferences
@ -90,10 +91,10 @@ class DefaultNotificationsRepositoryTest {
@Test
fun `GIVEN stored application id WHEN getApplicationId THEN returns it`() = runTest {
// GIVEN
val expectedAppId = "test-app-id"
val expectedAppId = ApplicationId("test-app-id")
val preferences = mockk<Preferences>(relaxed = true)
val key = stringPreferencesKey(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY.name)
every { preferences[key] } returns expectedAppId
every { preferences[key] } returns expectedAppId.value
coEvery { preferencesDataStore.data } returns flowOf(preferences)
// WHEN
@ -103,75 +104,24 @@ class DefaultNotificationsRepositoryTest {
assertThat(result).isEqualTo(expectedAppId)
}
@Test
fun `GIVEN application id and wallet list WHEN associateApplicationIdWithWallets THEN associates them`() = runTest {
// GIVEN
val appId = "test-app-id"
val wallets = listOf("wallet1", "wallet2")
coEvery {
tangemTechApi.associateApplicationIdWithWallets(
appId,
wallets.map { WalletIdBody(it) },
)
} returns ApiResponse.Success(Unit)
// WHEN
repository.associateApplicationIdWithWallets(appId, wallets)
// THEN
coVerify { tangemTechApi.associateApplicationIdWithWallets(appId, wallets.map { WalletIdBody(it) }) }
}
@Test
fun `GIVEN wallet id and name WHEN setWalletName THEN updates wallet name`() = runTest {
// GIVEN
val walletId = "test-wallet-id"
val walletName = "Test Wallet"
coEvery {
tangemTechApi.updateWallet(
walletId,
WalletBody(name = walletName),
)
} returns ApiResponse.Success(Unit)
// WHEN
repository.setWalletName(walletId, walletName)
// THEN
coVerify { tangemTechApi.updateWallet(walletId, WalletBody(name = walletName)) }
}
@Test
fun `GIVEN wallet id WHEN getWalletName THEN returns wallet name`() = runTest {
// GIVEN
val walletId = "test-wallet-id"
val expectedName = "Test Wallet"
coEvery { tangemTechApi.getWalletById(walletId) } returns ApiResponse.Success(
WalletResponse(
notifyStatus = false,
name = expectedName,
id = walletId,
),
)
// WHEN
val result = repository.getWalletName(walletId)
// THEN
assertThat(result).isEqualTo(expectedName)
}
@Test
fun `GIVEN application id and push token WHEN sendPushToken THEN updates push token`() = runTest {
// GIVEN
val appId = "test-app-id"
val appId = ApplicationId("test-app-id")
val pushToken = "test-push-token"
coEvery {
tangemTechApi.updatePushTokenForApplicationId(
appId,
NotificationApplicationCreateBody(pushToken = pushToken),
appId.value,
NotificationApplicationCreateBody(
pushToken = pushToken,
platform = null,
device = null,
systemVersion = null,
language = null,
timezone = null,
),
)
} returns ApiResponse.Success(appId)
} returns ApiResponse.Success(appId.value)
// WHEN
repository.sendPushToken(appId, pushToken)
@ -179,8 +129,15 @@ class DefaultNotificationsRepositoryTest {
// THEN
coVerify {
tangemTechApi.updatePushTokenForApplicationId(
appId,
NotificationApplicationCreateBody(pushToken = pushToken),
appId.value,
NotificationApplicationCreateBody(
pushToken = pushToken,
platform = null,
device = null,
systemVersion = null,
language = null,
timezone = null,
),
)
}
}

View file

@ -2,8 +2,10 @@ package com.tangem.data.quotes.di
import com.tangem.data.quotes.multi.DefaultMultiQuoteFetcher
import com.tangem.data.quotes.multi.DefaultMultiQuoteUpdater
import com.tangem.data.quotes.single.DefaultSingleQuoteFetcher
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.quotes.multi.MultiQuoteUpdater
import com.tangem.domain.quotes.single.SingleQuoteFetcher
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -21,4 +23,8 @@ internal interface QuoteFetcherModule {
@Binds
@Singleton
fun bindMultiQuoteUpdater(impl: DefaultMultiQuoteUpdater): MultiQuoteUpdater
@Binds
@Singleton
fun bindSingleQuoteFetcher(impl: DefaultSingleQuoteFetcher): SingleQuoteFetcher
}

View file

@ -0,0 +1,17 @@
package com.tangem.data.quotes.single
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.quotes.single.SingleQuoteFetcher
import javax.inject.Inject
internal class DefaultSingleQuoteFetcher @Inject constructor(
private val multiQuoteFetcher: MultiQuoteFetcher,
) : SingleQuoteFetcher {
override suspend fun invoke(params: SingleQuoteFetcher.Params) = multiQuoteFetcher.invoke(
MultiQuoteFetcher.Params(
currenciesIds = setOf(params.rawCurrencyId),
appCurrencyId = params.appCurrencyId,
),
)
}

View file

@ -0,0 +1,174 @@
package com.tangem.data.quotes.single
import com.google.common.truth.Truth
import com.tangem.common.test.data.quote.MockQuoteResponseFactory
import com.tangem.data.quotes.multi.DefaultMultiQuoteFetcher
import com.tangem.data.quotes.store.QuotesStoreV2
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
import com.tangem.domain.quotes.single.SingleQuoteFetcher
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifyOrder
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.math.BigDecimal
internal class DefaultSingleQuoteFetcherTest {
private val tangemTechApi = mockk<TangemTechApi>(relaxed = true)
private val appCurrencyResponseStore = mockk<AppCurrencyResponseStore>(relaxed = true)
private val quotesStore = mockk<QuotesStoreV2>(relaxed = true)
private val multiFetcher = DefaultMultiQuoteFetcher(
tangemTechApi = tangemTechApi,
appCurrencyResponseStore = appCurrencyResponseStore,
quotesStore = quotesStore,
dispatchers = TestingCoroutineDispatcherProvider(),
)
private val singleFetcher = DefaultSingleQuoteFetcher(multiFetcher)
@Test
fun `fetch single quote successfully`() = runTest {
val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null)
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
val coinIds = "BTC"
coEvery {
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
} returns ApiResponse.Success(successResponse)
val actual = singleFetcher(params)
coVerifyOrder {
quotesStore.refresh(currenciesIds = setOf(params.rawCurrencyId))
appCurrencyResponseStore.getSyncOrNull()
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
quotesStore.storeActual(values = successResponse.quotes)
}
coVerify(inverse = true) {
quotesStore.storeError(currenciesIds = any())
}
Truth.assertThat(actual.isRight()).isTrue()
}
@Test
fun `fetch single quote successfully if appCurrencyId from params is not null`() = runTest {
val appCurrencyId = "usd"
val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId)
val coinIds = "BTC"
coEvery {
tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds)
} returns ApiResponse.Success(successResponse)
val actual = singleFetcher(params)
coVerifyOrder {
quotesStore.refresh(currenciesIds = setOf(currenciesId))
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
quotesStore.storeActual(values = successResponse.quotes)
}
Truth.assertThat(actual.isRight()).isTrue()
}
@Test
fun `fetch single quote failure because appCurrencyId from params is blank`() = runTest {
val appCurrencyId = ""
val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId)
val actual = singleFetcher(params)
coVerifyOrder {
quotesStore.refresh(currenciesIds = setOf(currenciesId))
quotesStore.storeError(currenciesIds = setOf(currenciesId))
}
Truth.assertThat(actual.isLeft()).isTrue()
Truth.assertThat(actual.leftOrNull()).isInstanceOf(IllegalStateException::class.java)
Truth.assertThat(actual.leftOrNull()).hasMessageThat()
.isEqualTo("Unable to get AppCurrency for updating quotes")
}
@Test
fun `fetch single quote failure because api request failed`() = runTest {
val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null)
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency
val coinIds = "BTC"
@Suppress("UNCHECKED_CAST")
val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException) as ApiResponse<QuotesResponse>
coEvery { tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) } returns errorResponse
val actual = singleFetcher(params)
coVerifyOrder {
quotesStore.refresh(currenciesIds = setOf(currenciesId))
appCurrencyResponseStore.getSyncOrNull()
tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds)
quotesStore.storeError(currenciesIds = setOf(currenciesId))
}
coVerify(inverse = true) {
quotesStore.storeActual(values = any())
}
Truth.assertThat(actual.isLeft()).isTrue()
}
@Test
fun `fetch single quote failure because app currency not found`() = runTest {
val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null)
coEvery { appCurrencyResponseStore.getSyncOrNull() } returns null
val actual = singleFetcher(params)
coVerifyOrder {
quotesStore.refresh(currenciesIds = setOf(currenciesId))
appCurrencyResponseStore.getSyncOrNull()
quotesStore.storeError(currenciesIds = setOf(currenciesId))
}
coVerify(inverse = true) {
tangemTechApi.getQuotes(currencyId = any(), coinIds = any())
quotesStore.storeActual(values = any())
}
Truth.assertThat(actual.isLeft()).isTrue()
}
private companion object {
val currenciesId = CryptoCurrency.RawID(value = "BTC")
val usdAppCurrency = CurrenciesResponse.Currency(
id = "USD".lowercase(),
code = "USD",
name = "US Dollar",
unit = "$",
type = "fiat",
rateBTC = "",
)
val successResponse = QuotesResponse(
quotes = mapOf(
"BTC" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE),
),
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.data.tokens.di
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.tokens.repository.*
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
@ -33,6 +34,7 @@ internal object TokensDataModule {
dispatchers: CoroutineDispatcherProvider,
expressServiceLoader: ExpressServiceLoader,
excludedBlockchains: ExcludedBlockchains,
cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
): CurrenciesRepository {
return DefaultCurrenciesRepository(
tangemTechApi = tangemTechApi,
@ -43,6 +45,7 @@ internal object TokensDataModule {
expressServiceLoader = expressServiceLoader,
dispatchers = dispatchers,
excludedBlockchains = excludedBlockchains,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
)
}
@ -74,6 +77,7 @@ internal object TokensDataModule {
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
): NetworksRepository {
return DefaultNetworksRepository(
networksStatusesStore = networksStatusesStore,
@ -83,6 +87,7 @@ internal object TokensDataModule {
cacheRegistry = cacheRegistry,
dispatchers = dispatchers,
excludedBlockchains = excludedBlockchains,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
)
}

View file

@ -6,7 +6,6 @@ import com.tangem.blockchainsdk.utils.*
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.common.currency.*
import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory
import com.tangem.data.tokens.utils.CustomTokensMerger
import com.tangem.data.tokens.utils.UserTokensBackwardCompatibility
import com.tangem.datasource.api.common.response.ApiResponseError
@ -51,12 +50,12 @@ internal class DefaultCurrenciesRepository(
private val expressServiceLoader: ExpressServiceLoader,
private val dispatchers: CoroutineDispatcherProvider,
private val excludedBlockchains: ExcludedBlockchains,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
) : CurrenciesRepository {
private val demoConfig = DemoConfig()
private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains)
private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains)
private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig, excludedBlockchains)
private val userTokensResponseFactory = UserTokensResponseFactory()
private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility()
private val customTokensMerger = CustomTokensMerger(tangemTechApi, dispatchers)
@ -223,7 +222,7 @@ internal class DefaultCurrenciesRepository(
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
val currency = cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse)
val currency = cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse)
fetchExpressAssetsByNetworkIds(userWalletId, listOf(currency), refresh)
currency
}
@ -237,8 +236,8 @@ internal class DefaultCurrenciesRepository(
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
val currencies = cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(
userWallet.scanResponse,
val currencies = cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(
scanResponse = userWallet.scanResponse,
)
fetchExpressAssetsByNetworkIds(userWalletId, currencies, refresh)
currencies
@ -253,7 +252,9 @@ internal class DefaultCurrenciesRepository(
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
val currency = cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
val currency = cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(
scanResponse = userWallet.scanResponse,
)
.find { it.id == id }
requireNotNull(currency) { "Unable to find currency with provided ID: $id" }
fetchExpressAssetsByNetworkIds(userWalletId, listOf(currency))
@ -672,7 +673,7 @@ internal class DefaultCurrenciesRepository(
private fun createDefaultUserTokensResponse(userWallet: UserWallet) =
userTokensResponseFactory.createUserTokensResponse(
currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse),
currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse),
isGroupedByNetwork = false,
isSortedByBalance = false,
)

View file

@ -5,8 +5,8 @@ import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory
import com.tangem.data.tokens.utils.NetworkStatusFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.network.NetworksStatusesStore
@ -15,7 +15,6 @@ import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.Network
@ -38,12 +37,11 @@ internal class DefaultNetworksRepository(
private val userWalletsStore: UserWalletsStore,
private val appPreferencesStore: AppPreferencesStore,
private val cacheRegistry: CacheRegistry,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
private val dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
) : NetworksRepository {
private val demoConfig = DemoConfig()
private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig, excludedBlockchains)
private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains)
private val networkStatusFactory = NetworkStatusFactory()
@ -225,10 +223,14 @@ internal class DefaultNetworksRepository(
responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse).asSequence()
} else {
if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(
scanResponse = userWallet.scanResponse,
)
.asSequence()
} else {
val currency = cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse)
val currency = cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(
scanResponse = userWallet.scanResponse,
)
sequenceOf(currency)
}

View file

@ -1,86 +0,0 @@
package com.tangem.data.tokens.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
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.demo.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
class CardCryptoCurrenciesFactory(
private val demoConfig: DemoConfig,
excludedBlockchains: ExcludedBlockchains,
) {
private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains)
fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List<CryptoCurrency.Coin> {
val card = scanResponse.card
var blockchains = if (demoConfig.isDemoCardId(card.cardId)) {
demoConfig.demoBlockchains
} else {
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}
if (card.isTestCard) {
blockchains = blockchains.mapNotNull { it.getTestnetVersion() }
}
return blockchains.mapNotNull {
cryptoCurrencyFactory.createCoin(
blockchain = it,
extraDerivationPath = null,
scanResponse = scanResponse,
)
}
}
fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency {
val resolver = scanResponse.cardTypesResolver
val blockchain = resolver.getBlockchain()
val coin = cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
)
requireNotNull(coin) { "Coin for the single currency card cannot be null" }
val primaryToken = resolver.getPrimaryToken()?.let { token ->
cryptoCurrencyFactory.createToken(
sdkToken = token,
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
)
}
return primaryToken ?: coin
}
fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List<CryptoCurrency> {
val resolver = scanResponse.cardTypesResolver
val blockchain = resolver.getBlockchain()
val coin = cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
)
requireNotNull(coin) { "Coin for the single currency card cannot be null" }
val primaryToken = resolver.getPrimaryToken()?.let { token ->
cryptoCurrencyFactory.createToken(
sdkToken = token,
blockchain = blockchain,
extraDerivationPath = null,
scanResponse = scanResponse,
)
}
return listOfNotNull(coin, primaryToken)
}
}

View file

@ -190,12 +190,19 @@ internal class DefaultTransactionRepository(
null
}
val contractAddress = when (val identifier = nftAsset.identifier) {
is NFTAsset.Identifier.EVM -> identifier.tokenAddress
is NFTAsset.Identifier.Solana -> identifier.tokenAddress
is NFTAsset.Identifier.TON -> identifier.tokenAddress
NFTAsset.Identifier.Unknown -> ""
}
return@withContext createTransaction(
amount = Amount(
value = nftAsset.amount?.toBigDecimal() ?: error("Invalid amount"),
token = Token(
symbol = blockchain.currency,
contractAddress = "",
contractAddress = contractAddress,
decimals = nftAsset.decimals ?: error("Invalid decimals"),
),
),

View file

@ -42,6 +42,7 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
@Suppress("TooManyFunctions")
internal object WalletConnectDataModule {
@Provides
@ -86,6 +87,7 @@ internal object WalletConnectDataModule {
dispatchers: CoroutineDispatcherProvider,
legacyStore: WalletConnectSessionsRepository,
getWallets: GetWalletsUseCase,
associateNetworks: AssociateNetworksDelegate,
): DefaultWcSessionsManager {
val scope = CoroutineScope(SupervisorJob() + dispatchers.io)
return DefaultWcSessionsManager(
@ -93,6 +95,7 @@ internal object WalletConnectDataModule {
dispatchers = dispatchers,
legacyStore = legacyStore,
getWallets = getWallets,
associateNetworks = associateNetworks,
scope = scope,
)
}
@ -121,12 +124,12 @@ internal object WalletConnectDataModule {
@Singleton
fun wcEthNetwork(
@SdkMoshi moshi: Moshi,
excludedBlockchains: ExcludedBlockchains,
sessionsManager: WcSessionsManager,
factories: WcEthNetwork.Factories,
namespaceConverter: WcEthNetwork.NamespaceConverter,
): WcEthNetwork = WcEthNetwork(
moshi = moshi,
excludedBlockchains = excludedBlockchains,
namespaceConverter = namespaceConverter,
sessionsManager = sessionsManager,
factories = factories,
)
@ -135,7 +138,7 @@ internal object WalletConnectDataModule {
@Singleton
fun wcSolanaNetwork(
@SdkMoshi moshi: Moshi,
excludedBlockchains: ExcludedBlockchains,
namespaceConverter: WcSolanaNetwork.NamespaceConverter,
sessionsManager: WcSessionsManager,
factories: WcSolanaNetwork.Factories,
walletManager: UserWalletManager,
@ -143,28 +146,28 @@ internal object WalletConnectDataModule {
moshi = moshi,
sessionsManager = sessionsManager,
factories = factories,
excludedBlockchains = excludedBlockchains,
namespaceConverter = namespaceConverter,
walletManager = walletManager,
)
@Provides
@Singleton
fun caipNamespaceDelegate(
diHelperBox: DiHelperBox,
namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>,
walletManagersFacade: WalletManagersFacade,
): CaipNamespaceDelegate = CaipNamespaceDelegate(
namespaceConverters = diHelperBox.converters,
namespaceConverters = namespaceConverters,
walletManagersFacade = walletManagersFacade,
)
@Provides
@Singleton
fun associateNetworksDelegate(
diHelperBox: DiHelperBox,
namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>,
getWallets: GetWalletsUseCase,
currenciesRepository: CurrenciesRepository,
): AssociateNetworksDelegate = AssociateNetworksDelegate(
namespaceConverters = diHelperBox.converters,
namespaceConverters = namespaceConverters,
getWallets = getWallets,
currenciesRepository = currenciesRepository,
)
@ -176,10 +179,16 @@ internal object WalletConnectDataModule {
ethNetwork,
solanaNetwork,
),
converters = setOf(
ethNetwork,
solanaNetwork,
),
)
@Provides
@Singleton
fun namespaceConverters(
ethNamespaceConverter: WcEthNetwork.NamespaceConverter,
solanaNamespaceConverter: WcSolanaNetwork.NamespaceConverter,
): Set<@JvmSuppressWildcards WcNamespaceConverter> = setOf(
ethNamespaceConverter,
solanaNamespaceConverter,
)
@Provides
@ -188,6 +197,20 @@ internal object WalletConnectDataModule {
return DefaultWcRequestUseCaseFactory(diHelperBox.handlers)
}
@Provides
@Singleton
fun wcEthNetworkNamespaceConverter(excludedBlockchains: ExcludedBlockchains): WcEthNetwork.NamespaceConverter {
return WcEthNetwork.NamespaceConverter(excludedBlockchains)
}
@Provides
@Singleton
fun wcSolanaNetworkNamespaceConverter(
excludedBlockchains: ExcludedBlockchains,
): WcSolanaNetwork.NamespaceConverter {
return WcSolanaNetwork.NamespaceConverter(excludedBlockchains)
}
@Provides
@Singleton
fun providesWcDisconnectUseCase(sessionsManager: WcSessionsManager): WcDisconnectUseCase {
@ -195,7 +218,6 @@ internal object WalletConnectDataModule {
}
internal class DiHelperBox(
val converters: Set<WcNamespaceConverter>,
val handlers: Set<WcRequestToUseCaseConverter>,
)
}

View file

@ -22,12 +22,10 @@ import jakarta.inject.Inject
internal class WcEthNetwork(
private val moshi: Moshi,
private val excludedBlockchains: ExcludedBlockchains,
private val sessionsManager: WcSessionsManager,
private val factories: Factories,
) : WcRequestToUseCaseConverter, WcNamespaceConverter {
override val namespaceKey: NamespaceKey = NamespaceKey("eip155")
private val namespaceConverter: NamespaceConverter,
) : WcRequestToUseCaseConverter {
override fun toWcMethodName(request: WcSdkSessionRequest): WcEthMethodName? {
val methodKey = request.request.method
@ -39,7 +37,7 @@ internal class WcEthNetwork(
val name = toWcMethodName(request) ?: return null
val method: WcEthMethod = name.toMethod(request) ?: return null
val session = sessionsManager.findSessionByTopic(request.topic) ?: return null
val network = toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null
val network = namespaceConverter.toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null
val accountAddress = when (method) {
is WcEthMethod.MessageSign -> method.account
is WcEthMethod.SendTransaction -> method.transaction.from
@ -100,24 +98,31 @@ internal class WcEthNetwork(
return WcEthMethod.SignTypedData(params = params, account = account, dataForSign = data)
}
override fun toNetwork(chainId: String, wallet: UserWallet): Network? {
return toNetwork(chainId, wallet, excludedBlockchains)
}
internal class NamespaceConverter constructor(
private val excludedBlockchains: ExcludedBlockchains,
) : WcNamespaceConverter {
override fun toBlockchain(chainId: CAIP2): Blockchain? {
if (chainId.namespace != namespaceKey.key) return null
val ethChainId = chainId.reference.toIntOrNull() ?: return null
return Blockchain.fromChainId(ethChainId)
}
override val namespaceKey: NamespaceKey = NamespaceKey("eip155")
override fun toCAIP2(network: Network): CAIP2? {
val blockchain = Blockchain.fromId(network.id.value)
if (!blockchain.isEvm()) return null
val chainId = blockchain.getChainId() ?: return null
return CAIP2(
namespace = namespaceKey.key,
reference = chainId.toString(),
)
override fun toNetwork(chainId: String, wallet: UserWallet): Network? {
return toNetwork(chainId, wallet, excludedBlockchains)
}
override fun toBlockchain(chainId: CAIP2): Blockchain? {
if (chainId.namespace != namespaceKey.key) return null
val ethChainId = chainId.reference.toIntOrNull() ?: return null
return Blockchain.fromChainId(ethChainId)
}
override fun toCAIP2(network: Network): CAIP2? {
val blockchain = Blockchain.fromId(network.id.value)
if (!blockchain.isEvm()) return null
val chainId = blockchain.getChainId() ?: return null
return CAIP2(
namespace = namespaceKey.key,
reference = chainId.toString(),
)
}
}
internal class Factories @Inject constructor(

View file

@ -26,11 +26,9 @@ internal class WcSolanaNetwork(
private val moshi: Moshi,
private val sessionsManager: WcSessionsManager,
private val factories: Factories,
private val excludedBlockchains: ExcludedBlockchains,
private val namespaceConverter: NamespaceConverter,
private val walletManager: UserWalletManager,
) : WcNamespaceConverter, WcRequestToUseCaseConverter {
override val namespaceKey: NamespaceKey = NamespaceKey("solana")
) : WcRequestToUseCaseConverter {
override fun toWcMethodName(request: WcSdkSessionRequest): WcSolanaMethodName? {
val methodKey = request.request.method
@ -42,7 +40,7 @@ internal class WcSolanaNetwork(
val name = toWcMethodName(request) ?: return null
val method: WcSolanaMethod = name.toMethod(request) ?: return null
val session = sessionsManager.findSessionByTopic(request.topic) ?: return null
val network = toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null
val network = namespaceConverter.toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null
val accountAddress = getAccountAddress(network)
val context = WcMethodUseCaseContext(
session = session,
@ -66,31 +64,38 @@ internal class WcSolanaNetwork(
}
}
override fun toBlockchain(chainId: CAIP2): Blockchain? {
if (chainId.namespace != namespaceKey.key) return null
return when (chainId.reference) {
MAINNET_CHAIN_ID -> Blockchain.Solana
TESTNET_CHAIN_ID -> Blockchain.SolanaTestnet
else -> null
}
}
internal class NamespaceConverter @Inject constructor(
private val excludedBlockchains: ExcludedBlockchains,
) : WcNamespaceConverter {
override fun toNetwork(chainId: String, wallet: UserWallet): Network? {
return toNetwork(chainId, wallet, excludedBlockchains)
}
override val namespaceKey: NamespaceKey = NamespaceKey("solana")
override fun toCAIP2(network: Network): CAIP2? {
val blockchain = Blockchain.fromId(network.id.value)
val chainId = when (blockchain) {
Blockchain.Solana -> MAINNET_CHAIN_ID
Blockchain.SolanaTestnet -> TESTNET_CHAIN_ID
else -> null
override fun toBlockchain(chainId: CAIP2): Blockchain? {
if (chainId.namespace != namespaceKey.key) return null
return when (chainId.reference) {
MAINNET_CHAIN_ID -> Blockchain.Solana
TESTNET_CHAIN_ID -> Blockchain.SolanaTestnet
else -> null
}
}
override fun toNetwork(chainId: String, wallet: UserWallet): Network? {
return toNetwork(chainId, wallet, excludedBlockchains)
}
override fun toCAIP2(network: Network): CAIP2? {
val blockchain = Blockchain.fromId(network.id.value)
val chainId = when (blockchain) {
Blockchain.Solana -> MAINNET_CHAIN_ID
Blockchain.SolanaTestnet -> TESTNET_CHAIN_ID
else -> null
}
chainId ?: return null
return CAIP2(
namespace = namespaceKey.key,
reference = chainId,
)
}
chainId ?: return null
return CAIP2(
namespace = namespaceKey.key,
reference = chainId,
)
}
private fun WcSolanaMethodName.toMethod(request: WcSdkSessionRequest): WcSolanaMethod? {

View file

@ -1,6 +1,7 @@
package com.tangem.data.walletconnect.pair
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.Wallet.Model.Namespace
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
@ -16,6 +17,16 @@ internal class AssociateNetworksDelegate constructor(
private val currenciesRepository: CurrenciesRepository,
) {
suspend fun associate(wallet: UserWallet, namespaces: Map<String, Namespace.Session>): Set<Network> {
val walletNetworks = getWalletNetworks(wallet)
val namespacesSet = namespaces.values.flatMap { proposal -> proposal.chains ?: listOf() }.toSet()
return namespacesSet.mapNotNullTo(mutableSetOf()) { chainId ->
val wcNetwork = namespaceConverters
.firstNotNullOfOrNull { it.toNetwork(chainId, wallet) } ?: return@mapNotNullTo null
walletNetworks.find { network -> wcNetwork.id == network.id }
}
}
@Throws(WcPairError.UnsupportedNetworks::class)
suspend fun associate(sessionProposal: Wallet.Model.SessionProposal): Map<UserWallet, ProposalNetwork> {
val userWallets = getWallets.invokeSync().filter { it.isMultiCurrency }
@ -31,9 +42,7 @@ internal class AssociateNetworksDelegate constructor(
requiredNamespaces: Set<String>,
optionalNamespaces: Set<String>,
): ProposalNetwork {
val walletNetworks = currenciesRepository.getMultiCurrencyWalletCurrenciesSync(wallet.walletId)
.filterIsInstance<CryptoCurrency.Coin>()
.map { it.network }
val walletNetworks = getWalletNetworks(wallet)
val unknownRequired = mutableSetOf<String>()
val missingRequired = mutableSetOf<Network>()
@ -74,6 +83,11 @@ internal class AssociateNetworksDelegate constructor(
)
}
private suspend fun getWalletNetworks(wallet: UserWallet): List<Network> =
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(wallet.walletId)
.filterIsInstance<CryptoCurrency.Coin>()
.map { it.network }
private fun Map<String, Wallet.Model.Namespace.Proposal>.setOfChainId(): Set<String> =
this.values.flatMap { proposal -> proposal.chains ?: listOf() }.toSet()

View file

@ -14,7 +14,6 @@ import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.walletconnect.usecase.pair.WcPairState
import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase
import com.tangem.domain.wallets.models.UserWallet
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -78,9 +77,11 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
sessionForApprove = sessionForApprove,
sdkSessionProposal = sdkSessionProposal,
).map { settledSession ->
val newSession = settledSession.session.toDomain(
val newSession = WcSession(
wallet = sessionForApprove.wallet,
sdkModel = WcSdkSessionConverter.convert(settledSession.session),
securityStatus = proposalState.dAppSession.securityStatus,
networks = sessionForApprove.network.toSet(),
)
sessionsManager.saveSession(newSession)
newSession
@ -141,14 +142,6 @@ internal class DefaultWcPairUseCase @AssistedInject constructor(
}
},)
private fun Wallet.Model.Session.toDomain(wallet: UserWallet, securityStatus: CheckDAppResult): WcSession {
return WcSession(
wallet = wallet,
sdkModel = WcSdkSessionConverter.convert(this),
securityStatus = securityStatus,
)
}
private sealed interface TerminalAction {
data class Approve(val sessionForApprove: WcSessionApprove) : TerminalAction
data object Reject : TerminalAction

View file

@ -6,6 +6,7 @@ import arrow.core.right
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate
import com.tangem.data.walletconnect.utils.WcSdkObserver
import com.tangem.data.walletconnect.utils.WcSdkSessionConverter
import com.tangem.datasource.local.walletconnect.WalletConnectStore
@ -29,6 +30,7 @@ internal class DefaultWcSessionsManager(
private val legacyStore: WalletConnectSessionsRepository,
private val getWallets: GetWalletsUseCase,
private val dispatchers: CoroutineDispatcherProvider,
private val associateNetworks: AssociateNetworksDelegate,
private val scope: CoroutineScope,
) : WcSessionsManager, WcSdkObserver {
@ -76,10 +78,12 @@ internal class DefaultWcSessionsManager(
?: return@withContext null
val sdkSession = WalletKit.getActiveSessionByTopic(topic) ?: return@withContext null
val wallet = storedSession.wallet
val networks = associateNetworks.associate(wallet, sdkSession.namespaces)
WcSession(
wallet = wallet,
sdkModel = WcSdkSessionConverter.convert(sdkSession),
securityStatus = storedSession.securityStatus,
networks = networks,
)
}
@ -114,7 +118,7 @@ internal class DefaultWcSessionsManager(
return mustSaveInNewStore.isNotEmpty()
}
private fun associate(
private suspend fun associate(
inSdk: List<Wallet.Model.Session>,
inStore: Set<WcSessionDTO>,
wallets: List<UserWallet>,
@ -122,7 +126,13 @@ internal class DefaultWcSessionsManager(
val wcSessions = inStore.mapNotNull { session ->
val wallet = wallets.find { it.walletId == session.walletId } ?: return@mapNotNull null
val sdkSession = inSdk.find { it.topic == session.topic } ?: return@mapNotNull null
WcSession(wallet = wallet, sdkModel = WcSdkSessionConverter.convert(sdkSession), session.securityStatus)
val networks = associateNetworks.associate(wallet, sdkSession.namespaces)
WcSession(
wallet = wallet,
sdkModel = WcSdkSessionConverter.convert(sdkSession),
securityStatus = session.securityStatus,
networks = networks,
)
}
return wcSessions
}

View file

@ -20,6 +20,7 @@ import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.WcSessionApprove
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.walletconnect.usecase.pair.WcPairState
import com.tangem.domain.wallets.models.UserWalletId
import io.mockk.coEvery
import io.mockk.coVerifyOrder
import io.mockk.mockk
@ -91,6 +92,7 @@ internal class DefaultWcPairUseCaseTest {
wallet = sessionForApprove.wallet,
sdkModel = WcSdkSessionConverter.convert(this),
securityStatus = CheckDAppResult.SAFE,
networks = setOf(),
)
private fun useCaseFactory() = DefaultWcPairUseCase(
@ -99,7 +101,7 @@ internal class DefaultWcPairUseCaseTest {
caipNamespaceDelegate = caipNamespaceDelegate,
sdkDelegate = sdkDelegate,
blockAidVerifier = blockAidVerifier,
pairRequest = WcPairRequest(url, source),
pairRequest = WcPairRequest(userWalletId = UserWalletId(""), uri = url, source = source),
)
@Before

View file

@ -36,6 +36,7 @@ dependencies {
implementation(deps.arrow.core)
/** tests */
testImplementation(projects.domain.models)
testImplementation(deps.test.junit)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.truth)

View file

@ -1,5 +1,8 @@
package com.tangem.data.wallets
import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter
import com.tangem.data.wallets.converters.WalletIdBodyConverter
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
@ -17,7 +20,9 @@ import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.UserWalletRemoteInfo
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.utils.WEEK_MILLIS
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -28,12 +33,14 @@ import kotlinx.coroutines.withContext
typealias SeedPhraseNotificationsStatuses = Map<UserWalletId, SeedPhraseNotificationsStatus>
@Suppress("TooManyFunctions")
internal class DefaultWalletsRepository(
private val appPreferencesStore: AppPreferencesStore,
private val tangemTechApi: TangemTechApi,
private val userWalletsStore: UserWalletsStore,
private val seedPhraseNotificationVisibilityStore: RuntimeStateStore<SeedPhraseNotificationsStatuses>,
private val dispatchers: CoroutineDispatcherProvider,
private val authProvider: AuthProvider,
) : WalletsRepository {
override suspend fun shouldSaveUserWalletsSync(): Boolean {
@ -254,6 +261,53 @@ internal class DefaultWalletsRepository(
}
}
override suspend fun setWalletName(walletId: String, walletName: String) = withContext(dispatchers.io) {
tangemTechApi.updateWallet(
walletId = walletId,
body = WalletBody(name = walletName),
).getOrThrow()
}
override suspend fun getWalletInfo(walletId: String): UserWalletRemoteInfo = withContext(dispatchers.io) {
UserWalletRemoteInfoConverter.convert(
value = tangemTechApi.getWalletById(walletId).getOrThrow(),
)
}
override suspend fun getWalletsInfo(applicationId: String, updateCache: Boolean): List<UserWalletRemoteInfo> =
withContext(dispatchers.io) {
tangemTechApi.getWallets(applicationId)
.getOrThrow()
.map { walletInfo ->
val userWallet = UserWalletRemoteInfoConverter.convert(
value = walletInfo,
)
if (updateCache) {
setNotificationsEnabledLocally(
userWalletId = userWallet.walletId,
isEnabled = userWallet.isNotificationsEnabled,
)
}
userWallet
}
}
override suspend fun associateWallets(applicationId: String, wallets: List<UserWallet>) =
withContext(dispatchers.io) {
val publicKeys = authProvider.getCardsPublicKeys()
val walletsBody = wallets.map { userWallet ->
WalletIdBodyConverter.convert(
userWallet = userWallet,
publicKeys = publicKeys.filterKeys { userWallet.cardsInWallet.contains(it) },
)
}
tangemTechApi.associateApplicationIdWithWallets(
applicationId = applicationId,
body = walletsBody,
).getOrThrow()
}
private suspend fun loadAndSaveNotificationsEnabled(userWalletId: UserWalletId): Boolean {
val walletResponse = tangemTechApi.getWalletById(walletId = userWalletId.stringValue).getOrThrow()
val isEnabled = walletResponse.notifyStatus

View file

@ -0,0 +1,16 @@
package com.tangem.data.wallets.converters
import com.tangem.datasource.api.tangemTech.models.WalletResponse
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.UserWalletRemoteInfo
import com.tangem.utils.converter.Converter
internal object UserWalletRemoteInfoConverter : Converter<WalletResponse, UserWalletRemoteInfo> {
override fun convert(value: WalletResponse): UserWalletRemoteInfo {
return UserWalletRemoteInfo(
walletId = UserWalletId(value.id),
name = value.name.orEmpty(),
isNotificationsEnabled = value.notifyStatus,
)
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.data.wallets.converters
import com.tangem.datasource.api.tangemTech.models.CardInfoBody
import com.tangem.datasource.api.tangemTech.models.WalletIdBody
import com.tangem.domain.wallets.models.UserWallet
internal object WalletIdBodyConverter {
fun convert(userWallet: UserWallet, publicKeys: Map<String, String>): WalletIdBody {
return WalletIdBody(
walletId = userWallet.walletId.stringValue,
name = userWallet.name,
cards = publicKeys.map {
CardInfoBody(
cardId = it.key,
cardPublicKey = it.value,
)
},
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.data.wallets.di
import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository
import com.tangem.data.wallets.DefaultWalletsRepository
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
@ -26,6 +27,7 @@ internal object WalletsDataModule {
tangemTechApi: TangemTechApi,
userWalletsStore: UserWalletsStore,
dispatchers: CoroutineDispatcherProvider,
authProvider: AuthProvider,
): WalletsRepository {
return DefaultWalletsRepository(
appPreferencesStore = appPreferencesStore,
@ -33,6 +35,7 @@ internal object WalletsDataModule {
userWalletsStore = userWalletsStore,
seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = emptyMap()),
dispatchers = dispatchers,
authProvider = authProvider,
)
}

View file

@ -18,6 +18,8 @@ import io.mockk.coVerify
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import com.google.common.truth.Truth.assertThat
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.domain.wallets.models.UserWallet
import org.junit.Before
import org.junit.Test
@ -48,6 +50,7 @@ class DefaultWalletsRepositoryTest {
userWalletsStore = mockk(),
seedPhraseNotificationVisibilityStore = mockk(),
dispatchers = dispatchers,
authProvider = mockk(),
)
}
@ -173,4 +176,131 @@ class DefaultWalletsRepositoryTest {
}
coVerify(exactly = 1) { preferencesDataStore.updateData(any()) }
}
@Test
fun `GIVEN API returns wallets WHEN getWalletsInfo THEN should return converted wallets and update cache if requested`() = runTest {
// GIVEN
val applicationId = "test_app_id"
val wallet1Id = "1234567890abcdef"
val wallet2Id = "fedcba0987654321"
val walletResponses = listOf(
WalletResponse(
id = wallet1Id,
notifyStatus = true,
),
WalletResponse(
id = wallet2Id,
notifyStatus = false,
),
)
coEvery { tangemTechApi.getWallets(applicationId) } returns ApiResponse.Success(walletResponses)
coEvery { preferencesDataStore.updateData(any()) } returns mockk<Preferences>()
// WHEN
val result = repository.getWalletsInfo(applicationId, updateCache = true)
// THEN
assertThat(result).hasSize(2)
assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id)
assertThat(result[0].isNotificationsEnabled).isTrue()
assertThat(result[1].walletId.stringValue).isEqualTo(wallet2Id)
assertThat(result[1].isNotificationsEnabled).isFalse()
coVerify(exactly = 1) { tangemTechApi.getWallets(applicationId) }
coVerify(exactly = 2) { preferencesDataStore.updateData(any()) }
}
@Test
fun `GIVEN API returns wallets WHEN getWalletsInfo with updateCache false THEN should return converted wallets without updating cache`() = runTest {
// GIVEN
val applicationId = "test_app_id"
val wallet1Id = "1234567890abcdef"
val walletResponses = listOf(
WalletResponse(
id = wallet1Id,
notifyStatus = true,
),
)
coEvery { tangemTechApi.getWallets(applicationId) } returns ApiResponse.Success(walletResponses)
// WHEN
val result = repository.getWalletsInfo(applicationId, updateCache = false)
// THEN
assertThat(result).hasSize(1)
assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id)
assertThat(result[0].isNotificationsEnabled).isTrue()
coVerify(exactly = 1) { tangemTechApi.getWallets(applicationId) }
coVerify(exactly = 0) { preferencesDataStore.updateData(any()) }
}
@Test
fun `GIVEN user wallets and application ID WHEN associateWallets THEN should convert and send to API`() = runTest {
// GIVEN
val applicationId = "test_app_id"
val wallet1Id = "1234567890abcdef"
val wallet2Id = "fedcba0987654321"
val card1PublicKey = "card1_public_key"
val card2PublicKey = "card2_public_key"
val userWallets = listOf(
mockk<UserWallet> {
every { cardsInWallet } returns setOf(card1PublicKey)
every { walletId } returns UserWalletId(wallet1Id)
every { name } returns "Wallet 1"
},
mockk<UserWallet> {
every { cardsInWallet } returns setOf(card2PublicKey)
every { walletId } returns UserWalletId(wallet2Id)
every { name } returns "Wallet 2"
},
)
val publicKeys = mapOf(
card1PublicKey to "public_key_1",
card2PublicKey to "public_key_2",
)
val authProvider = mockk<AuthProvider> {
every { getCardsPublicKeys() } returns publicKeys
}
repository = DefaultWalletsRepository(
appPreferencesStore = appPreferenceStore,
tangemTechApi = tangemTechApi,
userWalletsStore = mockk(),
seedPhraseNotificationVisibilityStore = mockk(),
dispatchers = dispatchers,
authProvider = authProvider,
)
coEvery {
tangemTechApi.associateApplicationIdWithWallets(
eq(applicationId),
any(),
)
} returns ApiResponse.Success(Unit)
// WHEN
repository.associateWallets(applicationId, userWallets)
// THEN
coVerify(exactly = 1) {
tangemTechApi.associateApplicationIdWithWallets(
eq(applicationId),
match { body ->
body.size == 2 &&
body.any {
it.walletId == wallet1Id && it.cards.any { card -> card.cardPublicKey == "public_key_1" } &&
it.name == "Wallet 1"
} &&
body.any {
it.walletId == wallet2Id && it.cards.any { card -> card.cardPublicKey == "public_key_2" } &&
it.name == "Wallet 2"
}
},
)
}
}
}

View file

@ -0,0 +1,80 @@
package com.tangem.data.wallets.converters
import com.tangem.datasource.api.tangemTech.models.CardInfoBody
import com.tangem.datasource.api.tangemTech.models.WalletIdBody
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.google.common.truth.Truth.assertThat
import io.mockk.mockk
import org.junit.Test
class WalletIdBodyConverterTest {
@Test
fun `GIVEN user wallet with cards WHEN convert THEN should return correct WalletIdBody`() {
// GIVEN
val walletId = UserWalletId("1234567890abcdef")
val walletName = "Test Wallet"
val userWallet = UserWallet(
walletId = walletId,
name = walletName,
cardsInWallet = setOf("card1", "card2"),
isMultiCurrency = true,
hasBackupError = false,
scanResponse = mockk(),
)
val publicKeys = mapOf(
"card1" to "public_key_1",
"card2" to "public_key_2",
)
// WHEN
val result = WalletIdBodyConverter.convert(userWallet, publicKeys)
// THEN
assertThat(result).isEqualTo(
WalletIdBody(
walletId = walletId.stringValue,
name = walletName,
cards = listOf(
CardInfoBody(
cardId = "card1",
cardPublicKey = "public_key_1",
),
CardInfoBody(
cardId = "card2",
cardPublicKey = "public_key_2",
),
),
),
)
}
@Test
fun `GIVEN user wallet without cards WHEN convert THEN should return WalletIdBody with empty cards list`() {
// GIVEN
val walletId = UserWalletId("1234567890abcdef")
val walletName = "Test Wallet"
val userWallet = UserWallet(
walletId = walletId,
name = walletName,
cardsInWallet = emptySet(),
isMultiCurrency = true,
hasBackupError = false,
scanResponse = mockk(),
)
val publicKeys = emptyMap<String, String>()
// WHEN
val result = WalletIdBodyConverter.convert(userWallet, publicKeys)
// THEN
assertThat(result).isEqualTo(
WalletIdBody(
walletId = walletId.stringValue,
name = walletName,
cards = emptyList(),
),
)
}
}

View file

@ -14,7 +14,6 @@ import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
@ -24,7 +23,6 @@ class SaveManagedTokensUseCase(
private val customTokensRepository: CustomTokensRepository,
private val walletManagersFacade: WalletManagersFacade,
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
private val derivationsRepository: DerivationsRepository,
private val stakingRepository: StakingRepository,
private val quotesRepository: QuotesRepository,
@ -102,20 +100,12 @@ class SaveManagedTokensUseCase(
val networkToUpdate = currenciesToAdd.map { it.network }
.subtract(existingCurrencies.map { it.network }.toSet())
if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
multiNetworkStatusFetcher(
MultiNetworkStatusFetcher.Params(
userWalletId = userWalletId,
networks = networksToUpdate + networkToUpdate,
),
)
} else {
networksRepository.getNetworkStatusesSync(
multiNetworkStatusFetcher(
MultiNetworkStatusFetcher.Params(
userWalletId = userWalletId,
networks = networksToUpdate + networkToUpdate,
refresh = true,
)
}
),
)
}
private suspend fun refreshUpdatedYieldBalances(

View file

@ -12,7 +12,6 @@ import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
@ -30,7 +29,6 @@ class SaveMarketTokensUseCase(
private val derivationsRepository: DerivationsRepository,
private val marketsTokenRepository: MarketsTokenRepository,
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
private val quotesRepository: QuotesRepository,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
@ -83,20 +81,12 @@ class SaveMarketTokensUseCase(
}
private suspend fun refreshUpdatedNetworks(userWalletId: UserWalletId, addedCurrencies: List<CryptoCurrency>) {
if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
multiNetworkStatusFetcher(
MultiNetworkStatusFetcher.Params(
userWalletId = userWalletId,
networks = addedCurrencies.map(CryptoCurrency::network).toSet(),
),
)
} else {
networksRepository.getNetworkStatusesSync(
multiNetworkStatusFetcher(
MultiNetworkStatusFetcher.Params(
userWalletId = userWalletId,
networks = addedCurrencies.map(CryptoCurrency::network).toSet(),
refresh = true,
)
}
),
)
}
private suspend fun refreshUpdatedYieldBalances(

View file

@ -0,0 +1,4 @@
package com.tangem.domain.notifications.models
@JvmInline
value class ApplicationId(val value: String)

View file

@ -1,6 +1,7 @@
package com.tangem.domain.notifications
import arrow.core.Either
import com.tangem.domain.notifications.models.ApplicationId
import com.tangem.domain.notifications.repository.NotificationsRepository
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@ -10,7 +11,7 @@ class GetApplicationIdUseCase(
) {
private val mutex = Mutex()
suspend operator fun invoke(): Either<Throwable, String> = Either.catch {
suspend operator fun invoke(): Either<Throwable, ApplicationId> = Either.catch {
val localApplicationId = notificationsRepository.getApplicationId()
if (localApplicationId != null) return@catch localApplicationId

View file

@ -1,31 +1,23 @@
package com.tangem.domain.notifications.repository
import com.tangem.domain.notifications.models.ApplicationId
import com.tangem.domain.notifications.models.NotificationsEligibleNetwork
interface NotificationsRepository {
@Throws
suspend fun createApplicationId(pushToken: String? = null): String
suspend fun createApplicationId(pushToken: String? = null): ApplicationId
suspend fun saveApplicationId(appId: String)
suspend fun saveApplicationId(appId: ApplicationId)
suspend fun getApplicationId(): String?
suspend fun getApplicationId(): ApplicationId?
suspend fun getTronTokenFeeNotificationShowCounter(): Int
suspend fun incrementTronTokenFeeNotificationShowCounter()
@Throws
suspend fun associateApplicationIdWithWallets(appId: String, wallets: List<String>)
@Throws
suspend fun setWalletName(walletId: String, walletName: String)
@Throws
suspend fun getWalletName(walletId: String): String?
@Throws
suspend fun sendPushToken(appId: String, pushToken: String)
suspend fun sendPushToken(appId: ApplicationId, pushToken: String)
@Throws
suspend fun getEligibleNetworks(): List<NotificationsEligibleNetwork>

View file

@ -2,6 +2,7 @@ package com.tangem.domain.notifications
import arrow.core.Either
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.notifications.models.ApplicationId
import com.tangem.domain.notifications.repository.NotificationsRepository
import io.mockk.coEvery
import io.mockk.coVerify
@ -20,7 +21,7 @@ class GetApplicationIdUseCaseTest {
@Test
fun `GIVEN local application ID exists WHEN invoke THEN return local application ID`() = runTest {
// GIVEN
val expectedApplicationId = "test-app-id"
val expectedApplicationId = ApplicationId("test-app-id")
coEvery { notificationsRepository.getApplicationId() } returns expectedApplicationId
// WHEN
@ -39,7 +40,7 @@ class GetApplicationIdUseCaseTest {
@Test
fun `GIVEN local application ID does not exist WHEN invoke THEN create and save new application ID`() = runTest {
// GIVEN
val newApplicationId = "new-app-id"
val newApplicationId = ApplicationId("new-app-id")
coEvery { notificationsRepository.getApplicationId() } returns null
coEvery { notificationsRepository.createApplicationId() } returns newApplicationId
coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit
@ -81,7 +82,7 @@ class GetApplicationIdUseCaseTest {
fun `GIVEN no local application ID WHEN multiple concurrent invokes THEN create only one application ID`() =
runTest {
// GIVEN
val newApplicationId = "new-app-id"
val newApplicationId = ApplicationId("new-app-id")
var isIdCreated = false
coEvery { notificationsRepository.getApplicationId() } answers {
@ -115,7 +116,7 @@ class GetApplicationIdUseCaseTest {
@Test
fun `GIVEN no local application ID WHEN multiple concurrent invokes with delay THEN create only one application ID`() = runTest {
// GIVEN
val newApplicationId = "new-app-id"
val newApplicationId = ApplicationId("new-app-id")
var isIdCreated = false
coEvery { notificationsRepository.getApplicationId() } answers {

View file

@ -2,6 +2,7 @@ package com.tangem.domain.notifications
import arrow.core.Either
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.notifications.models.ApplicationId
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import io.mockk.coEvery
@ -33,7 +34,7 @@ class SendPushTokenUseCaseTest {
@Test
fun `GIVEN valid application ID and token WHEN invoke THEN token is sent successfully`() = runTest {
// GIVEN
val applicationId = "test-app-id"
val applicationId = ApplicationId("test-app-id")
val token = "test-token"
coEvery { getApplicationIdUseCase() } returns Either.Right(applicationId)
coEvery { pushNotificationsTokenProvider.getToken() } returns token
@ -62,7 +63,7 @@ class SendPushTokenUseCaseTest {
@Test
fun `GIVEN repository throws error WHEN invoke THEN returns error`() = runTest {
// GIVEN
val applicationId = "test-app-id"
val applicationId = ApplicationId("test-app-id")
val token = "test-token"
val expectedError = RuntimeException("Network error")
coEvery { getApplicationIdUseCase() } returns Either.Right(applicationId)

View file

@ -0,0 +1,22 @@
package com.tangem.domain.quotes.single
import com.tangem.domain.core.flow.FlowFetcher
import com.tangem.domain.tokens.model.CryptoCurrency
/**
* Fetcher of quote for [CryptoCurrency.RawID]
*
[REDACTED_AUTHOR]
*/
interface SingleQuoteFetcher : FlowFetcher<SingleQuoteFetcher.Params> {
/**
* Params
*
* @property rawCurrencyId crypto currency id
*/
data class Params(
val rawCurrencyId: CryptoCurrency.RawID,
val appCurrencyId: String?,
)
}

View file

@ -12,7 +12,6 @@ import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.async
@ -29,7 +28,6 @@ import kotlinx.coroutines.coroutineScope
@Suppress("LongParameterList")
class AddCryptoCurrenciesUseCase(
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
private val quotesRepository: QuotesRepository,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
@ -139,26 +137,13 @@ class AddCryptoCurrenciesUseCase(
}
?.network
if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
multiNetworkStatusFetcher(
MultiNetworkStatusFetcher.Params(
userWalletId = userWalletId,
networks = setOfNotNull(networksToUpdate, networkToUpdate),
),
)
} else {
catch(
{
networksRepository.getNetworkStatusesSync(
userWalletId = userWalletId,
networks = setOfNotNull(networksToUpdate, networkToUpdate),
refresh = true,
)
},
) {
raise(it)
}
}
multiNetworkStatusFetcher(
MultiNetworkStatusFetcher.Params(
userWalletId = userWalletId,
networks = setOfNotNull(networksToUpdate, networkToUpdate),
),
)
.bind()
}
private suspend fun refreshUpdatedYieldBalances(userWalletId: UserWalletId, addedCurrency: CryptoCurrency) {

View file

@ -13,7 +13,6 @@ import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.async
@ -23,7 +22,6 @@ import kotlinx.coroutines.coroutineScope
@Suppress("LongParameterList")
class FetchCardTokenListUseCase(
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
private val quotesRepository: QuotesRepository,
private val stakingRepository: StakingRepository,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
@ -41,7 +39,6 @@ class FetchCardTokenListUseCase(
fetchNetworksStatuses(
userWalletId = userWalletId,
networks = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::network),
refresh = refresh,
)
}
val fetchQuotes = async {
@ -80,19 +77,12 @@ class FetchCardTokenListUseCase(
private suspend fun Raise<TokenListError>.fetchNetworksStatuses(
userWalletId: UserWalletId,
networks: Set<Network>,
refresh: Boolean,
) {
if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
multiNetworkStatusFetcher(
MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks),
)
.mapLeft { TokenListError.DataError(it) }
} else {
catch(
block = { networksRepository.getNetworkStatusesSync(userWalletId, networks, refresh) },
catch = { raise(TokenListError.DataError(it)) },
)
}
multiNetworkStatusFetcher(
MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks),
)
.mapLeft(TokenListError::DataError)
.bind()
}
private suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.RawID>, refresh: Boolean) {

View file

@ -13,7 +13,6 @@ import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.async
@ -26,14 +25,12 @@ import kotlinx.coroutines.coroutineScope
* by providing a specific currency ID or fetching the status of the primary currency.
*
* @param currenciesRepository The repository for retrieving currency-related data.
* @param networksRepository The repository for retrieving network-related data.
* @param quotesRepository The repository for retrieving cryptocurrency quotes.
*/
// TODO: Add tests
@Suppress("LongParameterList")
class FetchCurrencyStatusUseCase(
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
private val quotesRepository: QuotesRepository,
private val stakingRepository: StakingRepository,
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
@ -86,7 +83,7 @@ class FetchCurrencyStatusUseCase(
refresh: Boolean,
) = coroutineScope {
val fetchStatus = async {
fetchNetworkStatus(userWalletId, currency.network, refresh)
fetchNetworkStatus(userWalletId, currency.network)
}
val fetchQuote = async {
fetchQuote(currency.id, refresh)
@ -120,23 +117,12 @@ class FetchCurrencyStatusUseCase(
}
}
private suspend fun Raise<CurrencyStatusError>.fetchNetworkStatus(
userWalletId: UserWalletId,
network: Network,
refresh: Boolean,
) {
if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
singleNetworkStatusFetcher(
params = SingleNetworkStatusFetcher.Params.Simple(userWalletId = userWalletId, network = network),
)
.mapLeft { CurrencyStatusError.DataError(it) }
} else {
catch(
block = { networksRepository.getNetworkStatusesSync(userWalletId, setOf(network), refresh) },
) {
raise(CurrencyStatusError.DataError(it))
}
}
private suspend fun Raise<CurrencyStatusError>.fetchNetworkStatus(userWalletId: UserWalletId, network: Network) {
singleNetworkStatusFetcher(
params = SingleNetworkStatusFetcher.Params.Simple(userWalletId = userWalletId, network = network),
)
.mapLeft { CurrencyStatusError.DataError(it) }
.bind()
}
private suspend fun Raise<CurrencyStatusError>.fetchQuote(currencyId: CryptoCurrency.ID, refresh: Boolean) {

View file

@ -15,7 +15,6 @@ import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.async
@ -27,7 +26,6 @@ import kotlinx.coroutines.coroutineScope
* network statuses, and quotes for tokens associated with a user's wallet.
*
* @param currenciesRepository The repository for retrieving currency-related data.
* @param networksRepository The repository for retrieving network-related data.
* @param quotesRepository The repository for retrieving cryptocurrency quotes.
* @param stakingRepository The repository for retrieving staking-related data.
*/
@ -35,7 +33,6 @@ import kotlinx.coroutines.coroutineScope
@Suppress("LongParameterList")
class FetchTokenListUseCase(
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
private val quotesRepository: QuotesRepository,
private val stakingRepository: StakingRepository,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
@ -71,7 +68,6 @@ class FetchTokenListUseCase(
fetchNetworksStatuses(
userWalletId = userWalletId,
networks = currencies.mapTo(hashSetOf()) { it.network },
refresh = mode.refreshNetworksStatuses,
)
}
val fetchQuotes = async {
@ -111,23 +107,12 @@ class FetchTokenListUseCase(
private suspend fun Raise<TokenListError>.fetchNetworksStatuses(
userWalletId: UserWalletId,
networks: Set<Network>,
refresh: Boolean,
) {
if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
if (refresh) {
multiNetworkStatusFetcher(
params = MultiNetworkStatusFetcher.Params(userWalletId, networks),
)
.mapLeft { TokenListError.DataError(it) }
.bind()
}
} else {
catch(
block = { networksRepository.getNetworkStatusesSync(userWalletId, networks, refresh) },
) {
raise(TokenListError.DataError(it))
}
}
multiNetworkStatusFetcher(
params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks),
)
.mapLeft(TokenListError::DataError)
.bind()
}
private suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean) {
@ -175,25 +160,21 @@ class FetchTokenListUseCase(
*/
enum class RefreshMode(
internal val refreshCurrencies: Boolean,
internal val refreshNetworksStatuses: Boolean,
internal val refreshQuotes: Boolean,
internal val refreshYieldBalances: Boolean,
) {
NONE(
refreshCurrencies = false,
refreshNetworksStatuses = false,
refreshQuotes = false,
refreshYieldBalances = false,
),
FULL(
refreshCurrencies = true,
refreshNetworksStatuses = true,
refreshQuotes = true,
refreshYieldBalances = true,
),
SKIP_CURRENCIES(
refreshCurrencies = false,
refreshNetworksStatuses = true,
refreshQuotes = true,
refreshYieldBalances = true,
),

View file

@ -1,30 +0,0 @@
package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.error.mapper.mapToCurrencyError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.wallets.models.UserWalletId
class GetCryptoCurrencyStatusSyncUseCase(
private val currencyStatusOperations: BaseCurrencyStatusOperations,
) {
// multi-currency
suspend operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
isSingleWalletWithTokens: Boolean = false,
): Either<CurrencyStatusError, CryptoCurrencyStatus> {
return currencyStatusOperations.getCurrencyStatusSync(userWalletId, cryptoCurrencyId, isSingleWalletWithTokens)
.mapLeft { error -> error.mapToCurrencyError() }
}
// single-currency
suspend operator fun invoke(userWalletId: UserWalletId): Either<CurrencyStatusError, CryptoCurrencyStatus> {
return currencyStatusOperations.getPrimaryCurrencyStatusSync(userWalletId)
.mapLeft { error -> error.mapToCurrencyError() }
}
}

View file

@ -1,62 +0,0 @@
package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.error.mapper.mapToCurrencyError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
/**
* Use case for fetching the status of a cryptocurrency associated with a user wallet.
*
*/
class GetCurrencyStatusUpdatesUseCase(
private val currencyStatusOperations: BaseCurrencyStatusOperations,
private val dispatchers: CoroutineDispatcherProvider,
) {
/**
* Invokes the use case.
*
* @param userWalletId The unique identifier of the user's wallet.
* @param currencyId The unique identifier of the cryptocurrency.
* @param isSingleWalletWithTokens Indicates whether the user wallet contains only one token on card (old cards)
* @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation.
*/
operator fun invoke(
userWalletId: UserWalletId,
currencyId: CryptoCurrency.ID,
isSingleWalletWithTokens: Boolean,
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
return flow {
emitAll(
getCurrencyStatus(
userWalletId = userWalletId,
currencyId = currencyId,
isSingleWalletWithTokens = isSingleWalletWithTokens,
),
)
}.flowOn(dispatchers.io)
}
private suspend fun getCurrencyStatus(
userWalletId: UserWalletId,
currencyId: CryptoCurrency.ID,
isSingleWalletWithTokens: Boolean,
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
val currencyFlow = currencyStatusOperations.getCurrencyStatusFlow(
userWalletId = userWalletId,
currencyId = currencyId,
isSingleWalletWithTokens = isSingleWalletWithTokens,
)
return currencyFlow.map { maybeCurrency ->
maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError)
}
}
}

View file

@ -1,17 +1,25 @@
package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.error.mapper.mapToTokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
class GetCryptoCurrencyStatusesSyncUseCase(
class GetMultiCryptoCurrencyStatusUseCase(
private val currencyStatusOperations: BaseCurrencyStatusOperations,
) {
suspend operator fun invoke(userWalletId: UserWalletId): Either<TokenListError, List<CryptoCurrencyStatus>> {
/**
* Returns synchronously list of cryptocurrency statuses for Multi-Currency wallet
*
* @param userWalletId The unique identifier of the user's wallet.
* @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation.
*/
suspend fun invokeMultiWalletSync(userWalletId: UserWalletId): Either<TokenListError, List<CryptoCurrencyStatus>> {
return currencyStatusOperations.getCurrenciesStatusesSync(userWalletId)
.mapLeft { error -> error.mapToTokenListError() }
}

View file

@ -1,42 +0,0 @@
package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.error.mapper.mapToCurrencyError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
/**
* Use case for fetching the status of the primary cryptocurrency associated with a user wallet.
*
* @property dispatchers Provides coroutine dispatchers.
*/
class GetPrimaryCurrencyStatusUpdatesUseCase(
private val dispatchers: CoroutineDispatcherProvider,
private val currencyStatusOperations: BaseCurrencyStatusOperations,
) {
/**
* Invokes the use case.
*
* @param userWalletId The unique identifier of the user's wallet.
* @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation.
*/
operator fun invoke(userWalletId: UserWalletId): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
return flow {
emitAll(getPrimaryCurrency(userWalletId))
}.flowOn(dispatchers.io)
}
private suspend fun getPrimaryCurrency(
userWalletId: UserWalletId,
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
return currencyStatusOperations.getPrimaryCurrencyStatusFlow(userWalletId).map { maybeCurrency ->
maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError)
}
}
}

View file

@ -0,0 +1,106 @@
package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.error.mapper.mapToCurrencyError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
/**
* Use case for fetching the status of a cryptocurrency associated with a user wallet.
*
*/
class GetSingleCryptoCurrencyStatusUseCase(
private val currencyStatusOperations: BaseCurrencyStatusOperations,
private val dispatchers: CoroutineDispatcherProvider,
) {
/**
* Returns cryptocurrency status flow for Multi-Currency wallet
*
* @param userWalletId The unique identifier of the user's wallet.
* @param currencyId The unique identifier of the cryptocurrency.
* @param isSingleWalletWithTokens Indicates whether the user wallet contains only one token on card (old cards)
* @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation.
*/
fun invokeMultiWallet(
userWalletId: UserWalletId,
currencyId: CryptoCurrency.ID,
isSingleWalletWithTokens: Boolean,
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
return flow {
emitAll(
getCurrencyStatus(
userWalletId = userWalletId,
currencyId = currencyId,
isSingleWalletWithTokens = isSingleWalletWithTokens,
),
)
}.flowOn(dispatchers.io)
}
/**
* Returns cryptocurrency status flow for primary currency for Single-Currency wallet
*
* @param userWalletId The unique identifier of the user's wallet.
* @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation.
*/
fun invokeSingleWallet(userWalletId: UserWalletId): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
return flow {
emitAll(
currencyStatusOperations.getPrimaryCurrencyStatusFlow(userWalletId).map { maybeCurrency ->
maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError)
},
)
}.flowOn(dispatchers.io)
}
/**
* Returns synchronously cryptocurrency status for Multi-Currency wallet
*
* @param userWalletId The unique identifier of the user's wallet.
* @param cryptoCurrencyId The unique identifier of the cryptocurrency.
* @param isSingleWalletWithTokens Indicates whether the user wallet contains only one token on card (old cards)
* @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation.
*/
suspend fun invokeMultiWalletSync(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
isSingleWalletWithTokens: Boolean = false,
): Either<CurrencyStatusError, CryptoCurrencyStatus> {
return currencyStatusOperations.getCurrencyStatusSync(userWalletId, cryptoCurrencyId, isSingleWalletWithTokens)
.mapLeft { error -> error.mapToCurrencyError() }
}
/**
* Returns synchronously cryptocurrency status for primary currency for Single-Currency wallet
*
* @param userWalletId The unique identifier of the user's wallet.
* @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation.
*/
suspend fun invokeSingleWalletSync(userWalletId: UserWalletId): Either<CurrencyStatusError, CryptoCurrencyStatus> {
return currencyStatusOperations.getPrimaryCurrencyStatusSync(userWalletId)
.mapLeft { error -> error.mapToCurrencyError() }
}
private suspend fun getCurrencyStatus(
userWalletId: UserWalletId,
currencyId: CryptoCurrency.ID,
isSingleWalletWithTokens: Boolean,
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
val currencyFlow = currencyStatusOperations.getCurrencyStatusFlow(
userWalletId = userWalletId,
currencyId = currencyId,
isSingleWalletWithTokens = isSingleWalletWithTokens,
)
return currencyFlow.map { maybeCurrency ->
maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError)
}
}
}

View file

@ -7,8 +7,6 @@ package com.tangem.domain.tokens
*/
interface TokensFeatureToggles {
val isNetworksLoadingRefactoringEnabled: Boolean
val isQuotesLoadingRefactoringEnabled: Boolean
val isStakingLoadingRefactoringEnabled: Boolean

View file

@ -2,12 +2,10 @@ package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.raise.Raise
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.delay
@ -15,14 +13,9 @@ import kotlinx.coroutines.delay
* Use case responsible for fetching currency status information, including network status
* and quotes for a given cryptocurrency. It provides methods to fetch currency status either
* by providing a specific currency ID or fetching the status of the primary currency.
*
* @param networksRepository The repository for retrieving network-related data.
*/
class UpdateDelayedNetworkStatusUseCase(
private val networksRepository: NetworksRepository,
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
/**
@ -30,37 +23,24 @@ class UpdateDelayedNetworkStatusUseCase(
*
* @param userWalletId The ID of the user's wallet.
* @param network Network of the cryptocurrency.
* @param refresh Indicates whether to force a refresh of the status data.
* @return An [Either] representing success (Right) or an error (Left) in fetching the status.
*/
suspend operator fun invoke(
userWalletId: UserWalletId,
network: Network,
delayMillis: Long = 0L,
refresh: Boolean = false,
): Either<CurrencyStatusError, Unit> {
delay(delayMillis)
return either {
fetchNetworkStatus(userWalletId, network, refresh)
fetchNetworkStatus(userWalletId, network)
}
}
private suspend fun Raise<CurrencyStatusError>.fetchNetworkStatus(
userWalletId: UserWalletId,
network: Network,
refresh: Boolean,
) {
if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
singleNetworkStatusFetcher(
params = SingleNetworkStatusFetcher.Params.Simple(userWalletId = userWalletId, network = network),
)
.mapLeft { CurrencyStatusError.DataError(it) }
} else {
catch(
block = { networksRepository.getNetworkStatusesSync(userWalletId, setOf(network), refresh) },
) {
raise(CurrencyStatusError.DataError(it))
}
}
private suspend fun Raise<CurrencyStatusError>.fetchNetworkStatus(userWalletId: UserWalletId, network: Network) {
singleNetworkStatusFetcher(
params = SingleNetworkStatusFetcher.Params.Simple(userWalletId = userWalletId, network = network),
)
.mapLeft(CurrencyStatusError::DataError)
.bind()
}
}

View file

@ -22,7 +22,6 @@ import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator
import com.tangem.domain.wallets.models.UserWalletId
@ -33,7 +32,6 @@ import kotlinx.coroutines.flow.*
*
* @property currenciesRepository repository for currencies
* @property quotesRepository repository for quotes
* @property networksRepository repository for networks
* @property stakingRepository repository for staking
*
[REDACTED_AUTHOR]
@ -43,7 +41,6 @@ abstract class BaseCurrencyStatusOperations(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val quotesRepositoryV2: QuotesRepositoryV2,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
@ -203,24 +200,14 @@ abstract class BaseCurrencyStatusOperations(
?.right()
?: Error.EmptyQuotes.left()
val networkStatuses = if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
singleNetworkStatusSupplier(
params = SingleNetworkStatusProducer.Params(
userWalletId = userWalletId,
network = currency.network,
),
)
.firstOrNull()
.right()
} else {
networksRepository.getNetworkStatusesSync(
val networkStatuses = singleNetworkStatusSupplier(
params = SingleNetworkStatusProducer.Params(
userWalletId = userWalletId,
networks = setOf(currency.network),
refresh = false,
)
.firstOrNull { it.network == currency.network }
.right()
}
network = currency.network,
),
)
.firstOrNull()
.right()
val yieldBalances = getYieldBalanceSync(userWalletId, currency)
@ -270,7 +257,7 @@ abstract class BaseCurrencyStatusOperations(
val nonEmptyCurrencies =
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId).toNonEmptyListOrNull()
?: return emptyList<CryptoCurrencyStatus>().right()
val (networks, currenciesIds) = getIds(nonEmptyCurrencies)
val (_, currenciesIds) = getIds(nonEmptyCurrencies)
val rawIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet()
val quotes = if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) {
@ -279,16 +266,13 @@ abstract class BaseCurrencyStatusOperations(
quotesRepository.getQuotesSync(rawIds, false).right()
}
val networkStatuses = if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
multiNetworkStatusSupplier(
params = MultiNetworkStatusProducer.Params(userWalletId = userWalletId),
)
.firstOrNull()
.orEmpty()
.right()
} else {
networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right()
}
val networkStatuses = multiNetworkStatusSupplier(
params = MultiNetworkStatusProducer.Params(userWalletId = userWalletId),
)
.firstOrNull()
.orEmpty()
.right()
val yieldBalances = getYieldBalancesSync(userWalletId, nonEmptyCurrencies)
return currencyStatusProxyCreator.createCurrenciesStatuses(
@ -326,25 +310,11 @@ abstract class BaseCurrencyStatusOperations(
)
}
val networkStatus = if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
singleNetworkStatusSupplier(
params = SingleNetworkStatusProducer.Params(
userWalletId = userWalletId,
network = currency.network,
),
)
.firstOrNull()
.right()
} else {
catch(
block = {
networksRepository.getNetworkStatusesSync(userWalletId, setOf(currency.network))
.firstOrNull { it.network == currency.network }
.right()
},
catch = { Error.DataError(it).left() },
)
}
val networkStatus = singleNetworkStatusSupplier(
params = SingleNetworkStatusProducer.Params(userWalletId = userWalletId, network = currency.network),
)
.firstOrNull()
.right()
val yieldBalances = getYieldBalanceSync(userWalletId, currency)

View file

@ -30,7 +30,6 @@ import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.tokens.utils.extractAddress
import com.tangem.domain.wallets.models.UserWalletId
@ -43,7 +42,6 @@ class CachedCurrenciesStatusesOperations(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
quotesRepositoryV2: QuotesRepositoryV2,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
@ -58,7 +56,6 @@ class CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
quotesRepositoryV2 = quotesRepositoryV2,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
@ -156,7 +153,7 @@ class CachedCurrenciesStatusesOperations(
combine(
flow = getQuotes(currenciesIds),
flow2 = getNetworksStatuses(userWalletId, networks),
flow2 = getNetworkStatusesUpdates(userWalletId, networks),
flow3 = getYieldBalances(userWalletId, currencies),
flow4 = fetchingState.map {
val state = it[userWalletId] ?: return@map false
@ -184,13 +181,9 @@ class CachedCurrenciesStatusesOperations(
coroutineScope {
awaitAll(
async {
if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
multiNetworkStatusFetcher(
params = MultiNetworkStatusFetcher.Params(userWalletId, networks),
)
} else {
networksRepository.fetchNetworkStatuses(userWalletId, networks)
}
multiNetworkStatusFetcher(
params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks),
)
},
async {
val rawCurrenciesIds = currenciesIds.mapNotNullTo(mutableSetOf()) { it.rawCurrencyId }
@ -327,44 +320,12 @@ class CachedCurrenciesStatusesOperations(
userWalletId: UserWalletId,
network: Network,
): EitherFlow<Error, Set<NetworkStatus>> {
return if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
singleNetworkStatusSupplier(
params = SingleNetworkStatusProducer.Params(userWalletId = userWalletId, network = network),
)
.map<NetworkStatus, Either<Error, Set<NetworkStatus>>> { setOf(it).right() }
.distinctUntilChanged()
.onEmpty { emit(Error.EmptyNetworksStatuses.left()) }
} else {
networksRepository.getNetworkStatusesUpdates(userWalletId, setOf(network))
.map<Set<NetworkStatus>, Either<Error, Set<NetworkStatus>>> { it.right() }
.retryWhen { cause, _ ->
emit(Error.DataError(cause).left())
// adding delay before retry to avoid spam when flow restarted
delay(RETRY_DELAY)
true
}
.distinctUntilChanged()
.onEmpty { emit(Error.EmptyNetworksStatuses.left()) }
}
}
private fun getNetworksStatuses(
userWalletId: UserWalletId,
networks: NonEmptySet<Network>,
): EitherFlow<TokenListError, Set<NetworkStatus>> {
return if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
getNetworkStatusesUpdates(userWalletId, networks)
} else {
networksRepository.getNetworkStatusesUpdates(userWalletId, networks)
.map<Set<NetworkStatus>, Either<TokenListError, Set<NetworkStatus>>> { it.right() }
.retryWhen { cause, _ ->
emit(TokenListError.DataError(cause).left())
// adding delay before retry to avoid spam when flow restarted
delay(RETRY_DELAY)
true
}
.distinctUntilChanged()
}
return singleNetworkStatusSupplier(
params = SingleNetworkStatusProducer.Params(userWalletId = userWalletId, network = network),
)
.map<NetworkStatus, Either<Error, Set<NetworkStatus>>> { setOf(it).right() }
.distinctUntilChanged()
.onEmpty { emit(Error.EmptyNetworksStatuses.left()) }
}
private fun getYieldBalances(

View file

@ -1,197 +0,0 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.core.error.DataError
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.mock.MockNetworks
import com.tangem.domain.tokens.mock.MockQuotes
import com.tangem.domain.tokens.mock.MockTokens
import com.tangem.domain.tokens.mock.MockTokensStates
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.MockCurrenciesRepository
import com.tangem.domain.tokens.repository.MockNetworksRepository
import com.tangem.domain.tokens.repository.MockQuotesRepository
import com.tangem.domain.tokens.repository.MockStakingRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.math.BigDecimal
internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest {
private val dispatchers = TestingCoroutineDispatcherProvider()
private val userWalletId = UserWalletId(value = null)
@Test
fun `when all data received then token should be received`() = runTest {
// Given
val expectedResult = MockTokensStates.loadedTokensStates.first().right()
val useCase = getUseCase()
// When
val result = useCase(userWalletId).first()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when token getting failed then error should be received`() = runTest {
// Given
val expectedResult = CurrencyStatusError.DataError(DataError.NetworkError.NoInternetConnection).left()
val useCase = getUseCase(token = DataError.NetworkError.NoInternetConnection.left())
// When
val result = useCase(userWalletId).first()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when quotes getting failed then currency with no quote status should be received`() = runTest {
// Given
val expectedResult = MockTokensStates.noQuotesTokensStatuses.first().right()
val useCase = getUseCase(quotes = flowOf(DataError.NetworkError.NoInternetConnection.left()))
// When
val result = useCase(userWalletId).first()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when networks statuses getting failed then error should be received`() = runTest {
// Given
val expectedResult = CurrencyStatusError.DataError(DataError.NetworkError.NoInternetConnection).left()
val useCase = getUseCase(statuses = flowOf(DataError.NetworkError.NoInternetConnection.left()))
// When
val result = useCase(userWalletId).first()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when networks statuses flow is empty then error should be received`() = runTest {
val expectedResult = CurrencyStatusError.UnableToCreateCurrency.left()
val useCase = getUseCase(statuses = flowOf())
// When
val result = useCase(userWalletId).first()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when quotes flow is empty then no quote status should be received`() = runTest {
val expectedResult = MockTokensStates.noQuotesTokensStatuses.first().right()
val useCase = getUseCase(quotes = flowOf(emptySet<Quote>().right()))
// When
val result = useCase(userWalletId).first()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when quotes are empty and statuses are verified then token without quote should be received`() = runTest {
val expectedResult = with(MockTokensStates.tokenState1) {
copy(
value = CryptoCurrencyStatus.NoQuote(
amount = BigDecimal.TEN,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
),
yieldBalance = null,
sources = CryptoCurrencyStatus.Sources(),
),
)
}
.right()
val useCase = getUseCase(
statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()),
quotes = flowOf(emptySet<Quote>().right()),
)
// When
val result = useCase(userWalletId).first()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when quotes are loaded and statuses are empty then error be received`() = runTest {
val expectedResult = CurrencyStatusError.UnableToCreateCurrency.left()
val useCase = getUseCase(
statuses = flowOf(emptySet<NetworkStatus>().right()),
quotes = flowOf(MockQuotes.quotes.right()),
)
// When
val result = useCase(userWalletId).first()
// Then
assertEquals(expectedResult, result)
}
private fun getUseCase(
token: Either<DataError, CryptoCurrency> = MockTokens.token1.right(),
removeCurrencyResult: Either<DataError, Unit> = Unit.right(),
quotes: Flow<Either<DataError, Set<Quote>>> = flowOf(MockQuotes.quotes.right()),
statuses: Flow<Either<DataError, Set<NetworkStatus>>> = flowOf(MockNetworks.verifiedNetworksStatuses.right()),
) = GetPrimaryCurrencyStatusUpdatesUseCase(
currencyStatusOperations = CachedCurrenciesStatusesOperations(
currenciesRepository = MockCurrenciesRepository(
sortTokensResult = Unit.right(),
removeCurrencyResult = removeCurrencyResult,
token = token,
tokens = flowOf(),
isGrouped = flowOf(),
isSortedByBalance = flowOf(),
),
quotesRepository = MockQuotesRepository(quotes),
networksRepository = MockNetworksRepository(statuses),
stakingRepository = MockStakingRepository(),
tokensFeatureToggles = object : TokensFeatureToggles {
override val isNetworksLoadingRefactoringEnabled: Boolean = false
override val isQuotesLoadingRefactoringEnabled: Boolean = false
override val isStakingLoadingRefactoringEnabled: Boolean = false
},
singleNetworkStatusSupplier = mockk(),
multiNetworkStatusFetcher = mockk(),
multiNetworkStatusSupplier = mockk(),
multiQuoteFetcher = mockk(),
singleQuoteSupplier = mockk(),
quotesRepositoryV2 = mockk(),
singleYieldBalanceSupplier = mockk(),
multiYieldBalanceFetcher = mockk(),
),
dispatchers = dispatchers,
)
}

View file

@ -7,12 +7,10 @@ import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.networks.single.SingleNetworkStatusProducer
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.transaction.error.AssociateAssetError
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
@ -23,9 +21,7 @@ class AssociateAssetUseCase(
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val walletManagersFacade: WalletManagersFacade,
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
suspend operator fun invoke(
@ -59,20 +55,13 @@ class AssociateAssetUseCase(
}
private suspend fun isBalanceZero(userWalletId: UserWalletId, currency: CryptoCurrency): Boolean {
val networkStatus = if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
singleNetworkStatusSupplier(
params = SingleNetworkStatusProducer.Params(
userWalletId = userWalletId,
network = currency.network,
),
)
.firstOrNull()
} else {
networksRepository.getNetworkStatusesSync(
val networkStatus = singleNetworkStatusSupplier(
params = SingleNetworkStatusProducer.Params(
userWalletId = userWalletId,
networks = setOf(currency.network),
).find { it.network == currency.network }
}
network = currency.network,
),
)
.firstOrNull()
val networkCoinAmountStatus = (networkStatus?.value as? NetworkStatus.Verified)
?.amounts

View file

@ -19,7 +19,6 @@ import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.demo.DemoTransactionSender
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.error.SendTransactionError
@ -33,7 +32,6 @@ class SendTransactionUseCase(
private val transactionRepository: TransactionRepository,
private val walletManagersFacade: WalletManagersFacade,
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
suspend operator fun invoke(
txsData: List<TransactionData>,
@ -80,16 +78,15 @@ class SendTransactionUseCase(
}
cardSdkConfigRepository.setLinkedTerminal(linkedTerminal)
return sendResult
.onRight {
if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
singleNetworkStatusFetcher(
params = SingleNetworkStatusFetcher.Params.Simple(
userWalletId = userWallet.walletId,
network = network,
),
)
}
singleNetworkStatusFetcher(
params = SingleNetworkStatusFetcher.Params.Simple(
userWalletId = userWallet.walletId,
network = network,
),
)
}
.fold(
ifRight = { result -> result.hashes.right() },

View file

@ -1,11 +1,13 @@
package com.tangem.domain.walletconnect.model
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.serialization.Serializable
@Serializable
data class WcPairRequest(
val uri: String,
val source: Source,
val userWalletId: UserWalletId,
) {
enum class Source { QR, DEEPLINK, CLIPBOARD, ETC }
}

View file

@ -1,11 +1,13 @@
package com.tangem.domain.walletconnect.model
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession
import com.tangem.domain.wallets.models.UserWallet
data class WcSession(
val wallet: UserWallet,
val networks: Set<Network>,
val sdkModel: WcSdkSession,
val securityStatus: CheckDAppResult,
)

View file

@ -26,6 +26,7 @@ dependencies {
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.notifications.models)
// endregion
// region Tangem libraries
@ -37,4 +38,11 @@ dependencies {
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
// end
// region Tests
testImplementation(deps.test.junit)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.truth)
testImplementation(deps.test.mockk)
// end
}

View file

@ -0,0 +1,7 @@
package com.tangem.domain.wallets.models
class UserWalletRemoteInfo(
val walletId: UserWalletId,
val name: String,
val isNotificationsEnabled: Boolean,
)

View file

@ -0,0 +1,56 @@
package com.tangem.domain.wallets.delegate
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.tangem.common.CompletionResult
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UpdateWalletError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.UserWalletRemoteInfo
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
class DefaultUserWalletsSyncDelegate(
private val userWalletsListManager: UserWalletsListManager,
private val dispatchers: CoroutineDispatcherProvider,
) : UserWalletsSyncDelegate {
override suspend fun syncWallet(userWalletId: UserWalletId, name: String): Either<UpdateWalletError, UserWallet> {
return renameUserWallet(userWalletId, name)
}
override suspend fun syncWallets(list: List<UserWalletRemoteInfo>): Either<UpdateWalletError, Unit> = either {
list.forEach { userWallet ->
renameUserWallet(userWallet.walletId, userWallet.name).bind()
}
}
// TODO remove dispatchers whnen UserWalletsListManager will be main safe
private suspend fun renameUserWallet(
userWalletId: UserWalletId,
name: String,
): Either<UpdateWalletError, UserWallet> = withContext(dispatchers.io) {
either {
val existingNames = userWalletsListManager.userWalletsSync
ensure(existingNames.none { it.name == name && it.walletId != userWalletId }) {
UpdateWalletError.NameAlreadyExists
}
val previousName = existingNames.firstOrNull { it.walletId == userWalletId }?.name.orEmpty()
if (previousName == name) {
raise(UpdateWalletError.NameAlreadyExists)
}
return@withContext when (
val result =
userWalletsListManager.update(userWalletId) { it.copy(name = name) }
) {
is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error))
is CompletionResult.Success -> result.data
}
}
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.domain.wallets.delegate
import arrow.core.Either
import com.tangem.domain.wallets.models.UpdateWalletError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.UserWalletRemoteInfo
interface UserWalletsSyncDelegate {
suspend fun syncWallet(userWalletId: UserWalletId, name: String): Either<UpdateWalletError, UserWallet>
suspend fun syncWallets(list: List<UserWalletRemoteInfo>): Either<UpdateWalletError, Unit>
}

View file

@ -1,9 +1,12 @@
package com.tangem.domain.wallets.repository
import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.UserWalletRemoteInfo
import kotlinx.coroutines.flow.Flow
@Suppress("TooManyFunctions")
interface WalletsRepository {
suspend fun shouldSaveUserWalletsSync(): Boolean
@ -43,4 +46,16 @@ interface WalletsRepository {
@Throws
suspend fun setNotificationsEnabled(userWalletId: UserWalletId, isEnabled: Boolean)
@Throws
suspend fun setWalletName(walletId: String, walletName: String)
@Throws
suspend fun getWalletInfo(walletId: String): UserWalletRemoteInfo
@Throws
suspend fun getWalletsInfo(applicationId: String, updateCache: Boolean = true): List<UserWalletRemoteInfo>
@Throws
suspend fun associateWallets(applicationId: String, wallets: List<UserWallet>)
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import com.tangem.domain.notifications.models.ApplicationId
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
class AssociateWalletsWithApplicationIdUseCase(
private val userWalletsListManager: UserWalletsListManager,
private val walletsRepository: WalletsRepository,
) {
suspend operator fun invoke(applicationId: ApplicationId): Either<Throwable, Unit> = Either.catch {
val wallets = userWalletsListManager.userWalletsSync
walletsRepository.associateWallets(applicationId.value, wallets)
}
}

View file

@ -2,38 +2,23 @@ package com.tangem.domain.wallets.usecase
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.tangem.common.CompletionResult
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate
import com.tangem.domain.wallets.models.UpdateWalletError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import com.tangem.domain.wallets.repository.WalletsRepository
/**
* Use case for rename user wallet
*
* @property userWalletsListManager user wallets list manager
*/
class RenameWalletUseCase(
private val userWalletsListManager: UserWalletsListManager,
private val dispatchers: CoroutineDispatcherProvider,
private val walletsRepository: WalletsRepository,
private val userWalletsSyncDelegate: UserWalletsSyncDelegate,
) {
suspend operator fun invoke(userWalletId: UserWalletId, name: String): Either<UpdateWalletError, UserWallet> =
withContext(dispatchers.io) {
either {
val existingNames = userWalletsListManager.userWalletsSync
ensure(existingNames.none { it.name == name && it.walletId != userWalletId }) {
UpdateWalletError.NameAlreadyExists
}
when (val result = userWalletsListManager.update(userWalletId) { it.copy(name = name) }) {
is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error))
is CompletionResult.Success -> result.data
}
either {
runCatching {
walletsRepository.setWalletName(userWalletId.stringValue, name)
}
userWalletsSyncDelegate.syncWallet(userWalletId, name).bind()
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.domain.notifications.models.ApplicationId
import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate
import com.tangem.domain.wallets.models.UpdateWalletError
import com.tangem.domain.wallets.repository.WalletsRepository
class UpdateRemoteWalletsInfoUseCase(
private val walletsRepository: WalletsRepository,
private val userWalletsSyncDelegate: UserWalletsSyncDelegate,
) {
suspend operator fun invoke(applicationId: ApplicationId): Either<UpdateWalletError, Unit> = either {
val walletsInfo = walletsRepository.getWalletsInfo(applicationId.value)
userWalletsSyncDelegate.syncWallets(walletsInfo).bind()
}
}

Some files were not shown because too many files have changed in this diff Show more