Updated on 2026-08-14
This commit is contained in:
commit
0470e1f010
1144 changed files with 48463 additions and 11166 deletions
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.data.account.converter
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.right
|
||||
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
|
|
@ -8,7 +9,10 @@ import com.tangem.domain.models.account.DerivationIndex
|
|||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
internal fun String.toAccountId(userWalletId: UserWalletId): AccountId {
|
||||
return AccountId.forCryptoPortfolio(value = this, userWalletId = userWalletId).getOrElse {
|
||||
return when {
|
||||
startsWith(AccountId.PaymentAccountIdPrefix) -> AccountId.forPaymentAccount(userWalletId).right()
|
||||
else -> AccountId.forCryptoPortfolio(value = this, userWalletId = userWalletId)
|
||||
}.getOrElse {
|
||||
error("Unable to create AccountId from value: $this. Cause: $it")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,9 @@
|
|||
package com.tangem.data.account.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.account.converter.AccountConverterFactoryContainer
|
||||
import com.tangem.data.account.fetcher.DefaultWalletAccountsFetcher
|
||||
import com.tangem.data.account.repository.AccountsExpandedDTO
|
||||
import com.tangem.data.account.repository.DefaultAccountsCRUDRepository
|
||||
import com.tangem.data.account.repository.DefaultAccountsExpandedRepository
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.account.store.ArchivedAccountsStoreFactory
|
||||
import com.tangem.data.account.tokens.DefaultMainAccountTokensMigration
|
||||
|
|
@ -17,14 +12,9 @@ import com.tangem.data.common.account.WalletAccountsSaver
|
|||
import com.tangem.data.common.cache.etag.ETagsStore
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.accounts.AccountTokenMigrationStore
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.mapWithStringKeyTypes
|
||||
import com.tangem.datasource.utils.setTypes
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.account.repository.AccountsExpandedRepository
|
||||
import com.tangem.domain.account.tokens.MainAccountTokensMigration
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -63,28 +53,6 @@ internal object AccountDataModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAccountsExpandedRepository(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
appScope: AppCoroutineScope,
|
||||
): AccountsExpandedRepository {
|
||||
val store = DataStoreFactory.create<Map<String, Set<AccountsExpandedDTO>>>(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
moshi = moshi,
|
||||
types = mapWithStringKeyTypes(valueTypes = setTypes<AccountsExpandedDTO>()),
|
||||
defaultValue = emptyMap(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "account_expanded_store") },
|
||||
scope = appScope,
|
||||
)
|
||||
|
||||
return DefaultAccountsExpandedRepository(
|
||||
store = store,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWalletAccountsFetcher(impl: DefaultWalletAccountsFetcher): WalletAccountsFetcher = impl
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.data.account.di
|
||||
|
||||
import com.tangem.data.account.repository.DefaultAccountsExpandedRepository
|
||||
import com.tangem.domain.account.repository.AccountsExpandedRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface AccountUtilsModule {
|
||||
|
||||
@Binds
|
||||
fun provideAccountsExpandedRepositoryFactory(
|
||||
factory: DefaultAccountsExpandedRepository.Factory,
|
||||
): AccountsExpandedRepository.Factory
|
||||
}
|
||||
|
|
@ -1,17 +1,28 @@
|
|||
package com.tangem.data.account.repository
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.account.converter.toAccountId
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.mapWithStringKeyTypes
|
||||
import com.tangem.datasource.utils.setTypes
|
||||
import com.tangem.domain.account.models.AccountExpandedState
|
||||
import com.tangem.domain.account.repository.AccountsExpandedRepository
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultAccountsExpandedRepository(
|
||||
internal class DefaultAccountsExpandedRepository constructor(
|
||||
private val store: DataStore<Map<String, Set<AccountsExpandedDTO>>>,
|
||||
) : AccountsExpandedRepository {
|
||||
|
||||
|
|
@ -41,7 +52,7 @@ internal class DefaultAccountsExpandedRepository(
|
|||
}
|
||||
|
||||
override suspend fun clearStore() {
|
||||
store.updateData { emptyMap() }
|
||||
store.updateData { map -> map.mapValues { emptySet() } }
|
||||
}
|
||||
|
||||
override suspend fun update(accountState: AccountExpandedState) {
|
||||
|
|
@ -59,6 +70,28 @@ internal class DefaultAccountsExpandedRepository(
|
|||
map.plus(walletId.stringValue to updatedSet)
|
||||
}
|
||||
}
|
||||
|
||||
internal class Factory @Inject constructor(
|
||||
@NetworkMoshi private val moshi: Moshi,
|
||||
@ApplicationContext private val context: Context,
|
||||
private val appScope: AppCoroutineScope,
|
||||
) : AccountsExpandedRepository.Factory {
|
||||
override fun create(storeFileName: String): DefaultAccountsExpandedRepository {
|
||||
val store = DataStoreFactory.create<Map<String, Set<AccountsExpandedDTO>>>(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
moshi = moshi,
|
||||
types = mapWithStringKeyTypes(valueTypes = setTypes<AccountsExpandedDTO>()),
|
||||
defaultValue = emptyMap(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = storeFileName) },
|
||||
scope = appScope,
|
||||
)
|
||||
|
||||
return DefaultAccountsExpandedRepository(
|
||||
store = store,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
|
|||
|
|
@ -7,22 +7,25 @@ plugins {
|
|||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.data.tokensync"
|
||||
namespace = "com.tangem.data.assetsdiscovery"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.domain.tokensync)
|
||||
api(projects.domain.assetsdiscovery)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.walletManager)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.data.common)
|
||||
implementation(projects.data.walletManager)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(projects.libs.tangemSdkApi)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
implementation(tangemDeps.blockchain)
|
||||
implementation(tangemDeps.card.core)
|
||||
|
||||
implementation(deps.androidx.datastore)
|
||||
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.tangem.data.assetsdiscovery
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.blockchainsdk.BlockchainSDKFactory
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.data.walletmanager.extensions.makePublicKey
|
||||
import com.tangem.domain.assetsdiscovery.AssetsDiscoveryFacade
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.getSyncStrict
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.config.curvesConfig
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultAssetsDiscoveryFacade @Inject constructor(
|
||||
private val blockchainSDKFactory: BlockchainSDKFactory,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : AssetsDiscoveryFacade {
|
||||
|
||||
override suspend fun getAssetsDiscoveryService(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): AssetsDiscoveryFacade.AssetsDiscoveryServiceInfo? = withContext(dispatchers.io) {
|
||||
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
|
||||
if (userWallet !is UserWallet.Hot) return@withContext null
|
||||
|
||||
val assetsDiscoveryServiceFactory = blockchainSDKFactory.getAssetsDiscoveryServiceFactorySync()
|
||||
?: return@withContext null
|
||||
|
||||
val blockchain = network.toBlockchain()
|
||||
val address = makeAddress(userWallet, blockchain, network.derivationPath.value)
|
||||
?: return@withContext null
|
||||
|
||||
AssetsDiscoveryFacade.AssetsDiscoveryServiceInfo(
|
||||
address = address,
|
||||
service = assetsDiscoveryServiceFactory.create(blockchain),
|
||||
)
|
||||
}
|
||||
|
||||
private fun makeAddress(hotWallet: UserWallet.Hot, blockchain: Blockchain, derivationPath: String?): String? {
|
||||
val curve = hotWallet.curvesConfig.primaryCurve(blockchain)
|
||||
val selectedWallet = hotWallet.wallets.orEmpty().firstOrNull { it.curve == curve }
|
||||
?: return null
|
||||
|
||||
val path = derivationPath?.let { DerivationPath(rawPath = it) }
|
||||
|
||||
val publicKey = if (path != null) {
|
||||
makePublicKey(
|
||||
seedKey = selectedWallet.publicKey,
|
||||
blockchain = blockchain,
|
||||
derivationPath = path,
|
||||
derivedWalletKeys = selectedWallet.derivedKeys,
|
||||
isWallet2 = true,
|
||||
) ?: return null
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return try {
|
||||
val addresses = if (publicKey != null) {
|
||||
blockchain.makeAddresses(
|
||||
walletPublicKey = publicKey.blockchainKey,
|
||||
pairPublicKey = null,
|
||||
curve = selectedWallet.curve,
|
||||
)
|
||||
} else {
|
||||
blockchain.makeAddresses(
|
||||
walletPublicKey = selectedWallet.publicKey,
|
||||
pairPublicKey = null,
|
||||
curve = selectedWallet.curve,
|
||||
)
|
||||
}
|
||||
addresses.find { it.type == AddressType.Default }?.value
|
||||
} catch (e: Throwable) {
|
||||
TangemLogger.w("Failed to derive address for $blockchain", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.tangem.data.assetsdiscovery.di
|
||||
|
||||
import com.tangem.blockchainsdk.BlockchainSDKFactory
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.data.assetsdiscovery.DefaultAssetsDiscoveryFacade
|
||||
import com.tangem.data.assetsdiscovery.repository.DefaultAssetsDiscoveryRepository
|
||||
import com.tangem.data.assetsdiscovery.store.AssetsDiscoveryStoreFactory
|
||||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.assetsdiscovery.AssetsDiscoveryFacade
|
||||
import com.tangem.domain.assetsdiscovery.repository.AssetsDiscoveryRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object AssetsDiscoveryDataModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAssetsDiscoveryFacade(
|
||||
blockchainSDKFactory: BlockchainSDKFactory,
|
||||
userWalletsListRepository: UserWalletsListRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): AssetsDiscoveryFacade = DefaultAssetsDiscoveryFacade(
|
||||
blockchainSDKFactory = blockchainSDKFactory,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAssetsDiscoveryRepository(
|
||||
assetsDiscoveryFacade: AssetsDiscoveryFacade,
|
||||
tangemTechApi: TangemTechApi,
|
||||
userWalletsListRepository: UserWalletsListRepository,
|
||||
networkFactory: NetworkFactory,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
assetsDiscoveryStoreFactory: AssetsDiscoveryStoreFactory,
|
||||
responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
): AssetsDiscoveryRepository = DefaultAssetsDiscoveryRepository(
|
||||
assetsDiscoveryFacade = assetsDiscoveryFacade,
|
||||
tangemTechApi = tangemTechApi,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
networkFactory = networkFactory,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
assetsDiscoveryStoreFactory = assetsDiscoveryStoreFactory,
|
||||
responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory,
|
||||
dispatchers = dispatchers,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
package com.tangem.data.tokensync.repository
|
||||
package com.tangem.data.assetsdiscovery.repository
|
||||
|
||||
import com.tangem.blockchain.assetsdiscovery.models.DiscoveredAsset
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.tokenbalance.models.TokenBalance
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.data.assetsdiscovery.store.AssetsDiscoveryStore
|
||||
import com.tangem.data.assetsdiscovery.store.AssetsDiscoveryStoreFactory
|
||||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.tokensync.store.TokenSyncStore
|
||||
import com.tangem.data.tokensync.store.TokenSyncStoreFactory
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
|
|
@ -14,15 +14,15 @@ 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.getObjectMapSync
|
||||
import com.tangem.domain.assetsdiscovery.AssetsDiscoveryFacade
|
||||
import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress
|
||||
import com.tangem.domain.assetsdiscovery.repository.AssetsDiscoveryRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.getSyncStrict
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokensync.model.TokenSyncProgress
|
||||
import com.tangem.domain.tokensync.repository.TokenSyncRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.async
|
||||
|
|
@ -30,42 +30,39 @@ import kotlinx.coroutines.awaitAll
|
|||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultTokenSyncRepository(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
internal class DefaultAssetsDiscoveryRepository(
|
||||
private val assetsDiscoveryFacade: AssetsDiscoveryFacade,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val networkFactory: NetworkFactory,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val tokenSyncStoreFactory: TokenSyncStoreFactory,
|
||||
private val assetsDiscoveryStoreFactory: AssetsDiscoveryStoreFactory,
|
||||
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
) : TokenSyncRepository {
|
||||
) : AssetsDiscoveryRepository {
|
||||
|
||||
private val semaphore = Semaphore(MAX_CONCURRENT_REQUESTS)
|
||||
private val progressStates = ConcurrentHashMap<String, MutableStateFlow<TokenSyncProgress>>()
|
||||
private val progressStates = ConcurrentHashMap<String, MutableStateFlow<AssetsDiscoveryProgress>>()
|
||||
|
||||
override fun observeSyncProgress(userWalletId: UserWalletId): Flow<TokenSyncProgress> {
|
||||
override fun observeDiscoveryProgress(userWalletId: UserWalletId): Flow<AssetsDiscoveryProgress> {
|
||||
return getProgressFlow(userWalletId)
|
||||
}
|
||||
|
||||
override fun acknowledgeCompletion(userWalletId: UserWalletId) {
|
||||
val key = userWalletId.stringValue
|
||||
val stateFlow = progressStates[key] ?: return
|
||||
stateFlow.value = TokenSyncProgress.Idle
|
||||
stateFlow.value = AssetsDiscoveryProgress.Idle
|
||||
progressStates.remove(key, stateFlow)
|
||||
}
|
||||
|
||||
override suspend fun getDiscoveredCurrencies(userWalletId: UserWalletId): List<CryptoCurrency> {
|
||||
val tokenSyncStore = tokenSyncStoreFactory.provide(userWalletId)
|
||||
val storedTokens = tokenSyncStore.get()
|
||||
val assetsDiscoveryStore = assetsDiscoveryStoreFactory.provide(userWalletId)
|
||||
val storedTokens = assetsDiscoveryStore.get()
|
||||
if (storedTokens.isEmpty()) return emptyList()
|
||||
|
||||
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
|
||||
|
|
@ -77,34 +74,34 @@ internal class DefaultTokenSyncRepository(
|
|||
}
|
||||
|
||||
override suspend fun clearDiscoveredTokens(userWalletId: UserWalletId) {
|
||||
val tokenSyncStore = tokenSyncStoreFactory.provide(userWalletId)
|
||||
tokenSyncStore.clear()
|
||||
val assetsDiscoveryStore = assetsDiscoveryStoreFactory.provide(userWalletId)
|
||||
assetsDiscoveryStore.clear()
|
||||
}
|
||||
|
||||
override suspend fun clearPendingFlag(userWalletId: UserWalletId) {
|
||||
setPendingFlag(userWalletId, value = false)
|
||||
}
|
||||
|
||||
override suspend fun getPendingSyncWalletIds(): List<UserWalletId> {
|
||||
override suspend fun getPendingDiscoveryWalletIds(): List<UserWalletId> {
|
||||
val pendingMap = appPreferencesStore
|
||||
.getObjectMapSync<Boolean>(PreferencesKeys.PENDING_DISCOVERY_SYNC_KEY)
|
||||
.getObjectMapSync<Boolean>(PreferencesKeys.PENDING_ASSETS_DISCOVERY_KEY)
|
||||
return pendingMap
|
||||
.filter { it.value }
|
||||
.map { UserWalletId(it.key) }
|
||||
}
|
||||
|
||||
override suspend fun runSync(userWalletId: UserWalletId) {
|
||||
override suspend fun runDiscovery(userWalletId: UserWalletId) {
|
||||
val networks = getSupportedNetworks(userWalletId)
|
||||
|
||||
if (networks.isEmpty()) return
|
||||
|
||||
setPendingFlag(userWalletId, value = true)
|
||||
val tokenSyncStore = tokenSyncStoreFactory.provide(userWalletId)
|
||||
tokenSyncStore.clear()
|
||||
val assetsDiscoveryStore = assetsDiscoveryStoreFactory.provide(userWalletId)
|
||||
assetsDiscoveryStore.clear()
|
||||
|
||||
val batches = networks.chunked(MAX_CONCURRENT_REQUESTS)
|
||||
var completedNetworks = 0
|
||||
getProgressFlow(userWalletId).value = TokenSyncProgress.InProgress(
|
||||
getProgressFlow(userWalletId).value = AssetsDiscoveryProgress.InProgress(
|
||||
completedNetworks = 0,
|
||||
totalNetworks = networks.size,
|
||||
)
|
||||
|
|
@ -114,25 +111,23 @@ internal class DefaultTokenSyncRepository(
|
|||
completedNetworks = handleBatchResults(
|
||||
userWalletId = userWalletId,
|
||||
results = batchResults,
|
||||
tokenSyncStore = tokenSyncStore,
|
||||
assetsDiscoveryStore = assetsDiscoveryStore,
|
||||
completedNetworks = completedNetworks,
|
||||
totalNetworks = networks.size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun completeSync(userWalletId: UserWalletId) {
|
||||
override suspend fun completeDiscovery(userWalletId: UserWalletId) {
|
||||
setPendingFlag(userWalletId, value = false)
|
||||
getProgressFlow(userWalletId).value = TokenSyncProgress.Completed
|
||||
getProgressFlow(userWalletId).value = AssetsDiscoveryProgress.Completed
|
||||
}
|
||||
|
||||
private suspend fun processBatch(userWalletId: UserWalletId, batch: List<Network>): List<NetworkResult> {
|
||||
return coroutineScope {
|
||||
batch.map { network ->
|
||||
async(dispatchers.io) {
|
||||
semaphore.withPermit {
|
||||
processNetwork(userWalletId, network)
|
||||
}
|
||||
processNetwork(userWalletId, network)
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
|
|
@ -141,7 +136,7 @@ internal class DefaultTokenSyncRepository(
|
|||
private suspend fun handleBatchResults(
|
||||
userWalletId: UserWalletId,
|
||||
results: List<NetworkResult>,
|
||||
tokenSyncStore: TokenSyncStore,
|
||||
assetsDiscoveryStore: AssetsDiscoveryStore,
|
||||
completedNetworks: Int,
|
||||
totalNetworks: Int,
|
||||
): Int {
|
||||
|
|
@ -150,8 +145,8 @@ internal class DefaultTokenSyncRepository(
|
|||
|
||||
for (result in results) {
|
||||
completed++
|
||||
handleNetworkResult(result, tokenSyncStore)
|
||||
progressFlow.value = TokenSyncProgress.InProgress(
|
||||
handleNetworkResult(result, assetsDiscoveryStore)
|
||||
progressFlow.value = AssetsDiscoveryProgress.InProgress(
|
||||
completedNetworks = completed,
|
||||
totalNetworks = totalNetworks,
|
||||
)
|
||||
|
|
@ -160,79 +155,78 @@ internal class DefaultTokenSyncRepository(
|
|||
return completed
|
||||
}
|
||||
|
||||
private suspend fun handleNetworkResult(result: NetworkResult, tokenSyncStore: TokenSyncStore) {
|
||||
private suspend fun handleNetworkResult(result: NetworkResult, assetsDiscoveryStore: AssetsDiscoveryStore) {
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
if (result.responseTokens.isNotEmpty()) {
|
||||
try {
|
||||
tokenSyncStore.append(result.responseTokens)
|
||||
assetsDiscoveryStore.append(result.responseTokens)
|
||||
} catch (e: Exception) {
|
||||
TangemLogger.e("Failed to store discovered tokens for network: ${result.networkId}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
is NetworkResult.Error -> {
|
||||
TangemLogger.e("Token sync failed for network: ${result.networkId}", result.cause)
|
||||
TangemLogger.e("Assets discovery failed for network: ${result.networkId}", result.cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun processNetwork(userWalletId: UserWalletId, network: Network): NetworkResult {
|
||||
return try {
|
||||
val tokenBalances = fetchAndFilterTokenBalances(userWalletId, network)
|
||||
val discoveredAssets = discoverAndFilterAssets(userWalletId, network)
|
||||
|
||||
if (tokenBalances.isEmpty()) {
|
||||
if (discoveredAssets.isEmpty()) {
|
||||
return NetworkResult.Success(
|
||||
networkId = network.backendId,
|
||||
networkId = network.rawId,
|
||||
responseTokens = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
val enrichedTokens = enrichTokensWithCatalog(tokenBalances, network)
|
||||
val enrichedTokens = enrichTokensWithCatalog(discoveredAssets, network)
|
||||
|
||||
val responseTokens = enrichedTokens
|
||||
.filter { it.contractAddress != null }
|
||||
.map { it.toResponseToken() }
|
||||
|
||||
NetworkResult.Success(
|
||||
networkId = network.backendId,
|
||||
networkId = network.rawId,
|
||||
responseTokens = responseTokens,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
NetworkResult.Error(networkId = network.backendId, cause = e)
|
||||
NetworkResult.Error(networkId = network.rawId, cause = e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchAndFilterTokenBalances(userWalletId: UserWalletId, network: Network): List<TokenBalance> {
|
||||
return withContext(dispatchers.io) {
|
||||
walletManagersFacade.getTokenBalances(userWalletId, network)
|
||||
private suspend fun discoverAndFilterAssets(userWalletId: UserWalletId, network: Network): List<DiscoveredAsset> =
|
||||
withContext(dispatchers.io) {
|
||||
val providerInfo = assetsDiscoveryFacade.getAssetsDiscoveryService(userWalletId, network)
|
||||
?: return@withContext emptyList()
|
||||
providerInfo.service.discoverAssets(providerInfo.address)
|
||||
.filter { it.amount > BigDecimal.ZERO }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun enrichTokensWithCatalog(
|
||||
tokenBalances: List<TokenBalance>,
|
||||
assets: List<DiscoveredAsset>,
|
||||
network: Network,
|
||||
): List<DiscoveredToken> = withContext(dispatchers.io) {
|
||||
val tokensToEnrich = tokenBalances.filter { !it.isNativeToken }
|
||||
): List<EnrichedDiscoveredAsset> = withContext(dispatchers.io) {
|
||||
val tokensToEnrich = assets.filterIsInstance<DiscoveredAsset.Token>()
|
||||
|
||||
val catalogMap = fetchCatalogInfo(
|
||||
networkId = network.backendId,
|
||||
contractAddresses = tokensToEnrich.mapNotNull(TokenBalance::contractAddress),
|
||||
networkId = network.rawId,
|
||||
contractAddresses = tokensToEnrich.map { it.contractAddress },
|
||||
)
|
||||
|
||||
tokenBalances.mapNotNull { balance ->
|
||||
if (balance.isNativeToken) return@mapNotNull null
|
||||
|
||||
val contractAddressLower = balance.contractAddress?.lowercase()
|
||||
val coin = contractAddressLower?.let { catalogMap[it] } ?: return@mapNotNull null
|
||||
tokensToEnrich.mapNotNull { balance ->
|
||||
val contractAddressLower = balance.contractAddress.lowercase()
|
||||
val coin = catalogMap[contractAddressLower] ?: return@mapNotNull null
|
||||
val decimals = coin.networks
|
||||
.find { it.contractAddress?.lowercase() == contractAddressLower }
|
||||
?.decimalCount
|
||||
?.toInt()
|
||||
?: 0
|
||||
|
||||
DiscoveredToken(
|
||||
EnrichedDiscoveredAsset(
|
||||
contractAddress = balance.contractAddress,
|
||||
symbol = coin.symbol,
|
||||
name = coin.name,
|
||||
|
|
@ -240,7 +234,7 @@ internal class DefaultTokenSyncRepository(
|
|||
amount = balance.amount,
|
||||
isNativeToken = false,
|
||||
currencyId = coin.id,
|
||||
networkId = network.backendId,
|
||||
networkId = network.rawId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -290,23 +284,23 @@ internal class DefaultTokenSyncRepository(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getProgressFlow(userWalletId: UserWalletId): MutableStateFlow<TokenSyncProgress> {
|
||||
private fun getProgressFlow(userWalletId: UserWalletId): MutableStateFlow<AssetsDiscoveryProgress> {
|
||||
return progressStates.getOrPut(userWalletId.stringValue) {
|
||||
MutableStateFlow(TokenSyncProgress.Idle)
|
||||
MutableStateFlow(AssetsDiscoveryProgress.Idle)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setPendingFlag(userWalletId: UserWalletId, value: Boolean) {
|
||||
appPreferencesStore.editData { prefs ->
|
||||
prefs.setObjectMap(
|
||||
key = PreferencesKeys.PENDING_DISCOVERY_SYNC_KEY,
|
||||
value = prefs.getObjectMap<Boolean>(PreferencesKeys.PENDING_DISCOVERY_SYNC_KEY)
|
||||
key = PreferencesKeys.PENDING_ASSETS_DISCOVERY_KEY,
|
||||
value = prefs.getObjectMap<Boolean>(PreferencesKeys.PENDING_ASSETS_DISCOVERY_KEY)
|
||||
.plus(userWalletId.stringValue to value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DiscoveredToken.toResponseToken(): UserTokensResponse.Token {
|
||||
private fun EnrichedDiscoveredAsset.toResponseToken(): UserTokensResponse.Token {
|
||||
return UserTokensResponse.Token(
|
||||
id = currencyId,
|
||||
networkId = networkId,
|
||||
|
|
@ -317,7 +311,7 @@ internal class DefaultTokenSyncRepository(
|
|||
)
|
||||
}
|
||||
|
||||
private data class DiscoveredToken(
|
||||
private data class EnrichedDiscoveredAsset(
|
||||
val contractAddress: String?,
|
||||
val symbol: String,
|
||||
val name: String,
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.data.tokensync.store
|
||||
package com.tangem.data.assetsdiscovery.store
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
|
||||
interface TokenSyncStore {
|
||||
interface AssetsDiscoveryStore {
|
||||
|
||||
suspend fun get(): List<UserTokensResponse.Token>
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.data.tokensync.store
|
||||
package com.tangem.data.assetsdiscovery.store
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
|
|
@ -17,18 +17,18 @@ import javax.inject.Inject
|
|||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class TokenSyncStoreFactory @Inject constructor(
|
||||
class AssetsDiscoveryStoreFactory @Inject constructor(
|
||||
@NetworkMoshi private val moshi: Moshi,
|
||||
@ApplicationContext private val context: Context,
|
||||
private val appScope: AppCoroutineScope,
|
||||
) {
|
||||
|
||||
private val stores = ConcurrentHashMap<String, TokenSyncStore>()
|
||||
private val stores = ConcurrentHashMap<String, AssetsDiscoveryStore>()
|
||||
|
||||
fun provide(userWalletId: UserWalletId): TokenSyncStore {
|
||||
fun provide(userWalletId: UserWalletId): AssetsDiscoveryStore {
|
||||
val userWalletStringId = userWalletId.formatted()
|
||||
return stores.computeIfAbsent(userWalletStringId) {
|
||||
DefaultTokenSyncStore(
|
||||
DefaultAssetsDiscoveryStore(
|
||||
persistenceStore = createPersistenceStore(
|
||||
fileName = "token_sync_$userWalletStringId",
|
||||
types = listTypes<UserTokensResponse.Token>(),
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
package com.tangem.data.tokensync.store
|
||||
package com.tangem.data.assetsdiscovery.store
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
|
||||
internal class DefaultTokenSyncStore(
|
||||
internal class DefaultAssetsDiscoveryStore(
|
||||
private val persistenceStore: DataStore<List<UserTokensResponse.Token>>,
|
||||
) : TokenSyncStore {
|
||||
) : AssetsDiscoveryStore {
|
||||
|
||||
override suspend fun get(): List<UserTokensResponse.Token> {
|
||||
return persistenceStore.data.firstOrNull().orEmpty()
|
||||
|
|
@ -33,6 +33,7 @@ internal class DefaultBlockAidRepository(
|
|||
return when (data.params) {
|
||||
is TransactionParams.Evm -> scanEvmTransaction(data = data)
|
||||
is TransactionParams.Solana -> scanSolanaTransaction(data = data)
|
||||
is TransactionParams.Bitcoin -> scanBitcoinTransaction(data = data)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -59,6 +60,17 @@ internal class DefaultBlockAidRepository(
|
|||
mapper.mapToDomain(response)
|
||||
}
|
||||
|
||||
@Suppress("UnusedParameter")
|
||||
private fun scanBitcoinTransaction(data: TransactionData): CheckTransactionResult {
|
||||
// TODO: BlockAid API doesn't support Bitcoin transaction scanning yet
|
||||
// When support is added, implement: api.scanBitcoinTransaction(mapper.mapToBitcoinRequest(data))
|
||||
return CheckTransactionResult(
|
||||
validation = com.domain.blockaid.models.transaction.ValidationResult.FAILED_TO_VALIDATE,
|
||||
description = "Bitcoin transaction validation is not yet supported by BlockAid",
|
||||
simulation = com.domain.blockaid.models.transaction.SimulationResult.FailedToSimulate,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun scanEvmTransactionBulk(
|
||||
blockchain: Blockchain,
|
||||
transactionDataList: List<SDKTransactionData.Uncompiled>,
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ class CryptoCurrencyFactory(
|
|||
id = cryptoCurrency.id.rawCurrencyId?.value,
|
||||
)
|
||||
val blockchain =
|
||||
Blockchain.fromNetworkId(cryptoCurrency.network.backendId) ?: Blockchain.Unknown
|
||||
Blockchain.fromNetworkId(cryptoCurrency.network.rawId) ?: Blockchain.Unknown
|
||||
val id = getTokenId(network, sdkToken)
|
||||
return CryptoCurrency.Token(
|
||||
id = id,
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ internal class DefaultCardCryptoCurrencyFactory(
|
|||
|
||||
// check if the blockchain of single-currency wallet is the same as network
|
||||
val cardNetworkId = userWallet.scanResponse.cardTypesResolver.getBlockchain().toNetworkId()
|
||||
val cardNetwork = networks.firstOrNull { it.backendId == cardNetworkId }
|
||||
val cardNetwork = networks.firstOrNull { it.rawId == cardNetworkId }
|
||||
|
||||
if (cardNetwork == null) return emptyMap()
|
||||
|
||||
|
|
@ -145,7 +145,7 @@ internal class DefaultCardCryptoCurrencyFactory(
|
|||
responseCryptoCurrenciesFactory.createCurrencies(
|
||||
tokens = accountDTO.tokens.orEmpty().filter { token ->
|
||||
networks.any {
|
||||
it.backendId == token.networkId && it.derivationPath.value == token.derivationPath
|
||||
it.rawId == token.networkId && it.derivationPath.value == token.derivationPath
|
||||
}
|
||||
},
|
||||
userWallet = userWallet,
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class UserTokensResponseFactory @Inject constructor() {
|
|||
UserTokensResponse.Token(
|
||||
id = id.rawCurrencyId?.value,
|
||||
accountId = accountId?.value,
|
||||
networkId = network.backendId,
|
||||
networkId = network.rawId,
|
||||
derivationPath = network.derivationPath.value,
|
||||
name = name,
|
||||
symbol = symbol,
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
package com.tangem.data.common.locale
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultLocaleProvider : LocaleProvider {
|
||||
|
||||
override fun getLocale(): Locale {
|
||||
return Locale.getDefault()
|
||||
}
|
||||
|
||||
override fun getWebUriLocaleLanguage(): String {
|
||||
val language = getLocale().language
|
||||
return if (LOCALE_LANG_RU.equals(language, true) || LOCALE_LANG_BY.equals(language, true)) {
|
||||
LOCALE_LANG_RU
|
||||
} else {
|
||||
LOCALE_LANG_EN
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LOCALE_LANG_RU = "ru"
|
||||
const val LOCALE_LANG_BY = "by"
|
||||
const val LOCALE_LANG_EN = "en"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
package com.tangem.data.common.locale
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface LocaleProvider {
|
||||
|
||||
fun getLocale(): Locale
|
||||
|
||||
fun getWebUriLocaleLanguage(): String
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.data.common.locale.di
|
||||
|
||||
import com.tangem.data.common.locale.DefaultLocaleProvider
|
||||
import com.tangem.data.common.locale.LocaleProvider
|
||||
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 LocaleProviderModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCacheRegistry(): LocaleProvider {
|
||||
return DefaultLocaleProvider()
|
||||
}
|
||||
}
|
||||
|
|
@ -136,8 +136,7 @@ class NetworkFactory @Inject constructor(
|
|||
|
||||
return runCatching {
|
||||
Network(
|
||||
id = Network.ID(value = blockchain.id, derivationPath = derivationPath),
|
||||
backendId = blockchain.toNetworkId(),
|
||||
id = Network.ID(value = blockchain.toNetworkId(), derivationPath = derivationPath),
|
||||
name = blockchain.fullName,
|
||||
isTestnet = blockchain.isTestnet(),
|
||||
derivationPath = derivationPath,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.data.common.network
|
|||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.common.test.domain.card.MockScanResponseFactory
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
||||
|
|
@ -187,7 +188,7 @@ class NetworkFactoryTest {
|
|||
userWallet = userWallet,
|
||||
expected = MockCryptoCurrencyFactory().ethereum.network.copy(
|
||||
id = Network.ID(
|
||||
value = Blockchain.Ethereum.id,
|
||||
value = Blockchain.Ethereum.toNetworkId(),
|
||||
derivationPath = expectedDerivationPath,
|
||||
),
|
||||
derivationPath = expectedDerivationPath,
|
||||
|
|
@ -201,12 +202,12 @@ class NetworkFactoryTest {
|
|||
derivationPath: Network.DerivationPath,
|
||||
): CreateTestModel.Second {
|
||||
return CreateTestModel.Second(
|
||||
networkId = Network.ID(value = Blockchain.Ethereum.id, derivationPath = derivationPath),
|
||||
networkId = Network.ID(value = Blockchain.Ethereum.toNetworkId(), derivationPath = derivationPath),
|
||||
derivationPath = derivationPath,
|
||||
userWallet = userWallet,
|
||||
expected = MockCryptoCurrencyFactory().ethereum.network.copy(
|
||||
id = Network.ID(
|
||||
value = Blockchain.Ethereum.id,
|
||||
value = Blockchain.Ethereum.toNetworkId(),
|
||||
derivationPath = derivationPath,
|
||||
),
|
||||
derivationPath = derivationPath,
|
||||
|
|
@ -227,7 +228,7 @@ class NetworkFactoryTest {
|
|||
derivationStyleProvider = derivationStyleProvider,
|
||||
canHandleTokens = canHandleTokens,
|
||||
expected = MockCryptoCurrencyFactory().ethereum.network.copy(
|
||||
id = Network.ID(value = Blockchain.Ethereum.id, derivationPath = expectedDerivationPath),
|
||||
id = Network.ID(value = Blockchain.Ethereum.toNetworkId(), derivationPath = expectedDerivationPath),
|
||||
derivationPath = expectedDerivationPath,
|
||||
canHandleTokens = canHandleTokens,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ android {
|
|||
namespace = "com.tangem.data.dynamicaddresses"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// region Project - Core
|
||||
implementation(projects.core.configToggles)
|
||||
|
|
@ -30,10 +34,16 @@ dependencies {
|
|||
|
||||
// region Project - Libs
|
||||
implementation(tangemDeps.blockchain) { exclude(module = "joda-time") }
|
||||
implementation(tangemDeps.card.core)
|
||||
// endregion
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
// region Testing
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(projects.test.core)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
package com.tangem.data.dynamicaddresses
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.DynamicAddressesManager
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository
|
||||
import com.tangem.domain.models.network.Network
|
||||
|
|
@ -33,13 +35,25 @@ internal class DefaultConsolidationRepository(
|
|||
|
||||
when (val result = dynamicAddressesManager.createConsolidationTransaction(fee)) {
|
||||
is Result.Success -> result.data
|
||||
is Result.Failure -> error("Failed to create consolidation tx: ${result.error}")
|
||||
is Result.Failure -> throw result.error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedParameter")
|
||||
private fun getNormalFee(walletManager: WalletManager): Fee {
|
||||
TODO("Fee calculation for consolidation from multiple addresses is not yet implemented")
|
||||
private suspend fun getNormalFee(walletManager: WalletManager): Fee {
|
||||
val coinAmount = walletManager.wallet.amounts[AmountType.Coin]
|
||||
?: error("Coin amount not found")
|
||||
val feeResult = walletManager.getFee(
|
||||
amount = coinAmount,
|
||||
destination = walletManager.wallet.address,
|
||||
)
|
||||
|
||||
return when (feeResult) {
|
||||
is Result.Success -> when (val txFee = feeResult.data) {
|
||||
is TransactionFee.Single -> txFee.normal
|
||||
is TransactionFee.Choosable -> txFee.normal
|
||||
}
|
||||
is Result.Failure -> throw feeResult.error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,55 +5,91 @@ import com.tangem.data.common.account.WalletAccountsSaver
|
|||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.dynamicaddresses.DynamicAddressesDerivationChecker
|
||||
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
|
||||
import com.tangem.domain.dynamicaddresses.DynamicAddressesSupportedBlockchains
|
||||
import com.tangem.domain.dynamicaddresses.GetDerivedXpubUseCase
|
||||
import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus
|
||||
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultDynamicAddressesRepository(
|
||||
private val walletAccountsFetcher: WalletAccountsFetcher,
|
||||
private val walletAccountsSaver: WalletAccountsSaver,
|
||||
private val accountsCRUDRepository: AccountsCRUDRepository,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
|
||||
private val getDerivedXpubUseCase: GetDerivedXpubUseCase,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : DynamicAddressesRepository {
|
||||
|
||||
private val extraFundsProbeCache = ConcurrentHashMap<Pair<UserWalletId, Network>, Boolean>()
|
||||
private val extraFundsProbeMutex = Mutex()
|
||||
|
||||
override fun getStatus(userWalletId: UserWalletId, network: Network): Flow<DynamicAddressesStatus> {
|
||||
return walletAccountsFetcher.get(userWalletId)
|
||||
.map { response ->
|
||||
val token = response.findToken(network)
|
||||
when {
|
||||
token?.dynamicAddressesEnabled == true -> DynamicAddressesStatus.ENABLED
|
||||
else -> DynamicAddressesStatus.DISABLED
|
||||
token?.dynamicAddressesEnabled != true -> DynamicAddressesStatus.DISABLED
|
||||
!walletManagersFacade.isDynamicAddressesEnabled(userWalletId, network) ->
|
||||
DynamicAddressesStatus.ENABLED_REQUIRES_SETUP
|
||||
else -> DynamicAddressesStatus.ENABLED
|
||||
}
|
||||
// TODO handle ENABLED_REQUIRES_SETUP when XPUB is not derived locally
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
}
|
||||
|
||||
override suspend fun enable(userWalletId: UserWalletId, network: Network, xpub: String) {
|
||||
withContext(dispatchers.io) {
|
||||
walletManagersFacade.enableXpubMode(userWalletId, network, xpub)
|
||||
val result = walletManagersFacade.enableXpubMode(userWalletId, network, xpub)
|
||||
if (result is SimpleResult.Failure) {
|
||||
error("Failed to enable xpub mode for $userWalletId / ${network.id}: ${result.error}")
|
||||
}
|
||||
updateTokenDynamicAddressesFlag(userWalletId, network, enabled = true)
|
||||
invalidateExtraFundsProbe(userWalletId, network)
|
||||
runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) }
|
||||
.onFailure { TangemLogger.e("Failed to sync tokens after DA enable for $userWalletId", it) }
|
||||
.onFailure { throwable ->
|
||||
TangemLogger.e(
|
||||
messageString = "Failed to sync tokens after dynamic addresses enable for $userWalletId",
|
||||
throwable = throwable,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun disable(userWalletId: UserWalletId, network: Network) {
|
||||
withContext(dispatchers.io) {
|
||||
walletManagersFacade.disableXpubMode(userWalletId, network)
|
||||
val result = walletManagersFacade.disableXpubMode(userWalletId, network)
|
||||
if (result is SimpleResult.Failure) {
|
||||
error("Failed to disable xpub mode for $userWalletId / ${network.id}: ${result.error}")
|
||||
}
|
||||
updateTokenDynamicAddressesFlag(userWalletId, network, enabled = false)
|
||||
invalidateExtraFundsProbe(userWalletId, network)
|
||||
runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) }
|
||||
.onFailure { TangemLogger.e("Failed to sync tokens after DA disable for $userWalletId", it) }
|
||||
.onFailure { throwable ->
|
||||
TangemLogger.e(
|
||||
messageString = "Failed to sync tokens after dynamic addresses disable for $userWalletId",
|
||||
throwable = throwable,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,6 +106,75 @@ internal class DefaultDynamicAddressesRepository(
|
|||
return walletManagersFacade.hasDynamicAddressesNonBaseBalances(userWalletId, network)
|
||||
}
|
||||
|
||||
override fun hasFundsOnAdditionalAddresses(userWalletId: UserWalletId, network: Network): Flow<Boolean> {
|
||||
if (!dynamicAddressesFeatureToggles.isDynamicAddressesEnabled) return flowOf(false)
|
||||
if (!DynamicAddressesSupportedBlockchains.isSupportedByNetworkId(network.id.rawId.value)) return flowOf(false)
|
||||
|
||||
return getStatus(userWalletId, network)
|
||||
.distinctUntilChanged()
|
||||
.map { status ->
|
||||
if (status != DynamicAddressesStatus.DISABLED) return@map false
|
||||
probeExtraFundsCached(userWalletId, network)
|
||||
}
|
||||
.onStart { emit(false) }
|
||||
.flowOn(dispatchers.io)
|
||||
}
|
||||
|
||||
private suspend fun probeExtraFundsCached(userWalletId: UserWalletId, network: Network): Boolean {
|
||||
val key = userWalletId to network
|
||||
extraFundsProbeCache[key]?.let { return it }
|
||||
return extraFundsProbeMutex.withLock {
|
||||
extraFundsProbeCache[key]?.let { return@withLock it }
|
||||
|
||||
val xpub = getDerivedXpubUseCase(userWalletId, network) ?: return@withLock false
|
||||
|
||||
val hasFunds = walletManagersFacade.probeHasFundsOnAdditionalAddresses(userWalletId, network, xpub)
|
||||
if (hasFunds) extraFundsProbeCache[key] = true
|
||||
hasFunds
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun invalidateExtraFundsProbe(userWalletId: UserWalletId, network: Network) {
|
||||
extraFundsProbeMutex.withLock {
|
||||
extraFundsProbeCache.remove(userWalletId to network)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun hasConflictingCustomTokens(userWalletId: UserWalletId, network: Network): Boolean {
|
||||
return withContext(dispatchers.io) {
|
||||
val response = walletAccountsFetcher.getSaved(userWalletId) ?: return@withContext false
|
||||
val baseDerivationPath = network.derivationPath.value ?: return@withContext false
|
||||
|
||||
response.accounts
|
||||
.flatMap { it.tokens.orEmpty() }
|
||||
.any { token ->
|
||||
val tokenDerivationPath = token.derivationPath ?: return@any false
|
||||
token.networkId == network.id.rawId.value &&
|
||||
tokenDerivationPath != baseDerivationPath &&
|
||||
DynamicAddressesDerivationChecker.hasSameAccountWithNonZeroChangeOrIndex(
|
||||
customPath = tokenDerivationPath,
|
||||
basePath = baseDerivationPath,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isDynamicAddressesEnabledForNetwork(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
): Flow<Boolean> {
|
||||
return walletAccountsFetcher.get(userWalletId)
|
||||
.map { response ->
|
||||
response.accounts
|
||||
.flatMap { it.tokens.orEmpty() }
|
||||
.any { token ->
|
||||
token.matchesNetwork(networkId) &&
|
||||
token.dynamicAddressesEnabled == true
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
}
|
||||
|
||||
private suspend fun updateTokenDynamicAddressesFlag(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
|
|
@ -80,7 +185,7 @@ internal class DefaultDynamicAddressesRepository(
|
|||
accounts = response.accounts.map { account ->
|
||||
account.copy(
|
||||
tokens = account.tokens?.map { token ->
|
||||
if (token.matchesNetwork(network)) {
|
||||
if (token.matchesNetwork(network.id)) {
|
||||
token.copy(dynamicAddressesEnabled = enabled)
|
||||
} else {
|
||||
token
|
||||
|
|
@ -95,12 +200,12 @@ internal class DefaultDynamicAddressesRepository(
|
|||
private fun GetWalletAccountsResponse.findToken(network: Network): UserTokensResponse.Token? {
|
||||
return accounts
|
||||
.flatMap { it.tokens.orEmpty() }
|
||||
.find { it.matchesNetwork(network) }
|
||||
.find { it.matchesNetwork(network.id) }
|
||||
}
|
||||
|
||||
private fun UserTokensResponse.Token.matchesNetwork(network: Network): Boolean {
|
||||
return networkId == network.backendId &&
|
||||
derivationPath == network.derivationPath.value &&
|
||||
private fun UserTokensResponse.Token.matchesNetwork(networkId: Network.ID): Boolean {
|
||||
return this.networkId == networkId.rawId.value &&
|
||||
derivationPath == networkId.derivationPath.value &&
|
||||
contractAddress == null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.data.dynamicaddresses
|
||||
|
||||
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
|
||||
import com.tangem.domain.dynamicaddresses.DynamicAddressesSupportedBlockchains
|
||||
import com.tangem.domain.dynamicaddresses.GetDerivedXpubUseCase
|
||||
import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus
|
||||
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Provides XPUB strings for networks that need dynamic addresses restore (ENABLED_REQUIRES_SETUP).
|
||||
* Uses only already-derived keys — no card scan triggered.
|
||||
*/
|
||||
@Singleton
|
||||
class DynamicAddressesInitializer @Inject constructor(
|
||||
private val dynamicAddressesRepository: DynamicAddressesRepository,
|
||||
private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
|
||||
private val getDerivedXpubUseCase: GetDerivedXpubUseCase,
|
||||
) {
|
||||
|
||||
suspend fun getXpubs(userWalletId: UserWalletId, networks: Set<Network>): Map<Network, String> {
|
||||
if (!dynamicAddressesFeatureToggles.isDynamicAddressesEnabled) return emptyMap()
|
||||
|
||||
val result = mutableMapOf<Network, String>()
|
||||
for (network in networks) {
|
||||
if (!DynamicAddressesSupportedBlockchains.isSupportedByNetworkId(network.rawId)) continue
|
||||
|
||||
val status = dynamicAddressesRepository.getStatus(userWalletId, network).firstOrNull()
|
||||
if (status != DynamicAddressesStatus.ENABLED_REQUIRES_SETUP) continue
|
||||
|
||||
val xpub = getDerivedXpubUseCase(userWalletId, network)
|
||||
if (xpub != null) {
|
||||
result[network] = xpub
|
||||
} else {
|
||||
TangemLogger.w("Dynamic addresses enabled but XPUB not available for ${network.id}")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import com.tangem.domain.account.repository.AccountsCRUDRepository
|
|||
import com.tangem.data.dynamicaddresses.DefaultDynamicAddressesFeatureToggles
|
||||
import com.tangem.data.dynamicaddresses.DefaultDynamicAddressesRepository
|
||||
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
|
||||
import com.tangem.domain.dynamicaddresses.GetDerivedXpubUseCase
|
||||
import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository
|
||||
import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
|
|
@ -29,6 +30,8 @@ internal object DynamicAddressesDataModule {
|
|||
walletAccountsSaver: WalletAccountsSaver,
|
||||
accountsCRUDRepository: AccountsCRUDRepository,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles,
|
||||
getDerivedXpubUseCase: GetDerivedXpubUseCase,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): DynamicAddressesRepository {
|
||||
return DefaultDynamicAddressesRepository(
|
||||
|
|
@ -36,6 +39,8 @@ internal object DynamicAddressesDataModule {
|
|||
walletAccountsSaver = walletAccountsSaver,
|
||||
accountsCRUDRepository = accountsCRUDRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dynamicAddressesFeatureToggles = dynamicAddressesFeatureToggles,
|
||||
getDerivedXpubUseCase = getDerivedXpubUseCase,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,218 @@
|
|||
package com.tangem.data.dynamicaddresses
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||
import com.tangem.data.common.account.WalletAccountsSaver
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles
|
||||
import com.tangem.domain.dynamicaddresses.GetDerivedXpubUseCase
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class DefaultDynamicAddressesRepositoryTest {
|
||||
|
||||
private val walletAccountsFetcher: WalletAccountsFetcher = mockk(relaxed = true)
|
||||
private val walletAccountsSaver: WalletAccountsSaver = mockk(relaxed = true)
|
||||
private val accountsCRUDRepository: AccountsCRUDRepository = mockk(relaxed = true)
|
||||
private val walletManagersFacade: WalletManagersFacade = mockk(relaxed = true)
|
||||
private val featureToggles: DynamicAddressesFeatureToggles = mockk(relaxed = true)
|
||||
private val getDerivedXpubUseCase: GetDerivedXpubUseCase = mockk(relaxed = true)
|
||||
|
||||
private val userWalletId: UserWalletId = mockk(relaxed = true)
|
||||
private val network: Network = mockk(relaxed = true) {
|
||||
every { id.rawId.value } returns SUPPORTED_NETWORK_ID
|
||||
}
|
||||
private val otherNetwork: Network = mockk(relaxed = true) {
|
||||
every { id.rawId.value } returns OTHER_SUPPORTED_NETWORK_ID
|
||||
}
|
||||
|
||||
private val dispatchers: CoroutineDispatcherProvider = TestDispatchers(Dispatchers.Unconfined)
|
||||
|
||||
private lateinit var repository: DefaultDynamicAddressesRepository
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
clearMocks(walletManagersFacade, featureToggles, getDerivedXpubUseCase, answers = false)
|
||||
// Empty response → findToken returns null → getStatus emits DISABLED.
|
||||
val emptyResponse = mockk<GetWalletAccountsResponse>(relaxed = true) {
|
||||
every { accounts } returns emptyList()
|
||||
}
|
||||
every { walletAccountsFetcher.get(userWalletId) } returns flowOf(emptyResponse)
|
||||
coEvery { walletManagersFacade.enableXpubMode(any(), any(), any()) } returns SimpleResult.Success
|
||||
coEvery { walletManagersFacade.disableXpubMode(any(), any()) } returns SimpleResult.Success
|
||||
|
||||
repository = DefaultDynamicAddressesRepository(
|
||||
walletAccountsFetcher = walletAccountsFetcher,
|
||||
walletAccountsSaver = walletAccountsSaver,
|
||||
accountsCRUDRepository = accountsCRUDRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dynamicAddressesFeatureToggles = featureToggles,
|
||||
getDerivedXpubUseCase = getDerivedXpubUseCase,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN feature toggle off WHEN collect THEN probe is never called and flow emits false`() = runTest {
|
||||
// GIVEN
|
||||
every { featureToggles.isDynamicAddressesEnabled } returns false
|
||||
|
||||
// WHEN
|
||||
val values = repository.hasFundsOnAdditionalAddresses(userWalletId, network).toList()
|
||||
|
||||
// THEN
|
||||
assertThat(values).doesNotContain(true)
|
||||
coVerify(exactly = 0) {
|
||||
walletManagersFacade.probeHasFundsOnAdditionalAddresses(any(), any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toggle on AND xpub is null WHEN collect THEN probe is not called AND cache is empty`() = runTest {
|
||||
// GIVEN
|
||||
every { featureToggles.isDynamicAddressesEnabled } returns true
|
||||
coEvery { getDerivedXpubUseCase(userWalletId, network) } returns null
|
||||
|
||||
// WHEN
|
||||
repository.hasFundsOnAdditionalAddresses(userWalletId, network).toList()
|
||||
// Second collect should invoke xpub derivation again (nothing is cached for null xpub).
|
||||
repository.hasFundsOnAdditionalAddresses(userWalletId, network).toList()
|
||||
|
||||
// THEN
|
||||
coVerify(exactly = 2) { getDerivedXpubUseCase(userWalletId, network) }
|
||||
coVerify(exactly = 0) {
|
||||
walletManagersFacade.probeHasFundsOnAdditionalAddresses(any(), any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN probe returns true WHEN collect THEN result is cached AND next collect skips probe`() = runTest {
|
||||
// GIVEN
|
||||
every { featureToggles.isDynamicAddressesEnabled } returns true
|
||||
coEvery { getDerivedXpubUseCase(userWalletId, network) } returns XPUB
|
||||
coEvery {
|
||||
walletManagersFacade.probeHasFundsOnAdditionalAddresses(userWalletId, network, XPUB)
|
||||
} returns true
|
||||
|
||||
// WHEN
|
||||
val firstValues = repository.hasFundsOnAdditionalAddresses(userWalletId, network).toList()
|
||||
val secondValues = repository.hasFundsOnAdditionalAddresses(userWalletId, network).toList()
|
||||
|
||||
// THEN
|
||||
assertThat(firstValues).contains(true)
|
||||
assertThat(secondValues).contains(true)
|
||||
coVerify(exactly = 1) {
|
||||
walletManagersFacade.probeHasFundsOnAdditionalAddresses(userWalletId, network, XPUB)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN probe returns false WHEN collect twice THEN probe runs each time`() = runTest {
|
||||
// GIVEN
|
||||
every { featureToggles.isDynamicAddressesEnabled } returns true
|
||||
coEvery { getDerivedXpubUseCase(userWalletId, network) } returns XPUB
|
||||
coEvery {
|
||||
walletManagersFacade.probeHasFundsOnAdditionalAddresses(userWalletId, network, XPUB)
|
||||
} returns false
|
||||
|
||||
// WHEN
|
||||
repository.hasFundsOnAdditionalAddresses(userWalletId, network).toList()
|
||||
repository.hasFundsOnAdditionalAddresses(userWalletId, network).toList()
|
||||
|
||||
// THEN — negative results are not cached, so the probe must re-run.
|
||||
coVerify(exactly = 2) {
|
||||
walletManagersFacade.probeHasFundsOnAdditionalAddresses(userWalletId, network, XPUB)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN cached true WHEN enable succeeds THEN cache is invalidated and next probe runs again`() = runTest {
|
||||
// GIVEN — populate cache with a positive probe
|
||||
every { featureToggles.isDynamicAddressesEnabled } returns true
|
||||
coEvery { getDerivedXpubUseCase(userWalletId, network) } returns XPUB
|
||||
coEvery {
|
||||
walletManagersFacade.probeHasFundsOnAdditionalAddresses(userWalletId, network, XPUB)
|
||||
} returns true
|
||||
repository.hasFundsOnAdditionalAddresses(userWalletId, network).first()
|
||||
|
||||
// WHEN — enable() succeeds and invalidates the cache
|
||||
repository.enable(userWalletId, network, XPUB)
|
||||
repository.hasFundsOnAdditionalAddresses(userWalletId, network).toList()
|
||||
|
||||
// THEN — probe was called once before enable, again after invalidation
|
||||
coVerify(exactly = 2) {
|
||||
walletManagersFacade.probeHasFundsOnAdditionalAddresses(userWalletId, network, XPUB)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN cached true WHEN disable succeeds THEN cache is invalidated and next probe runs again`() = runTest {
|
||||
// GIVEN
|
||||
every { featureToggles.isDynamicAddressesEnabled } returns true
|
||||
coEvery { getDerivedXpubUseCase(userWalletId, network) } returns XPUB
|
||||
coEvery {
|
||||
walletManagersFacade.probeHasFundsOnAdditionalAddresses(userWalletId, network, XPUB)
|
||||
} returns true
|
||||
repository.hasFundsOnAdditionalAddresses(userWalletId, network).first()
|
||||
|
||||
// WHEN
|
||||
repository.disable(userWalletId, network)
|
||||
repository.hasFundsOnAdditionalAddresses(userWalletId, network).toList()
|
||||
|
||||
// THEN
|
||||
coVerify(exactly = 2) {
|
||||
walletManagersFacade.probeHasFundsOnAdditionalAddresses(userWalletId, network, XPUB)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN cache entry for one network WHEN invalidate other network THEN first entry is preserved`() = runTest {
|
||||
// GIVEN — cache populated for `network`
|
||||
every { featureToggles.isDynamicAddressesEnabled } returns true
|
||||
coEvery { getDerivedXpubUseCase(userWalletId, network) } returns XPUB
|
||||
coEvery {
|
||||
walletManagersFacade.probeHasFundsOnAdditionalAddresses(userWalletId, network, XPUB)
|
||||
} returns true
|
||||
repository.hasFundsOnAdditionalAddresses(userWalletId, network).first()
|
||||
|
||||
// WHEN — disable invalidates a different network
|
||||
repository.disable(userWalletId, otherNetwork)
|
||||
repository.hasFundsOnAdditionalAddresses(userWalletId, network).toList()
|
||||
|
||||
// THEN — `network` cache is untouched; probe was called only once (initial fill)
|
||||
coVerify(exactly = 1) {
|
||||
walletManagersFacade.probeHasFundsOnAdditionalAddresses(userWalletId, network, XPUB)
|
||||
}
|
||||
}
|
||||
|
||||
private class TestDispatchers(dispatcher: CoroutineDispatcher) : CoroutineDispatcherProvider {
|
||||
override val main: CoroutineDispatcher = dispatcher
|
||||
override val mainImmediate: CoroutineDispatcher = dispatcher
|
||||
override val io: CoroutineDispatcher = dispatcher
|
||||
override val default: CoroutineDispatcher = dispatcher
|
||||
override val single: CoroutineDispatcher = dispatcher
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val XPUB = "xpub6-test-value"
|
||||
const val SUPPORTED_NETWORK_ID = "bitcoin"
|
||||
const val OTHER_SUPPORTED_NETWORK_ID = "litecoin"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.data.feedback
|
||||
|
||||
import android.os.Build
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.core.navigation.email.EmailSender
|
||||
import com.tangem.data.feedback.converters.BlockchainInfoConverter
|
||||
import com.tangem.data.feedback.converters.WalletMetaInfoConverter
|
||||
|
|
@ -10,12 +9,12 @@ import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
|||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.feedback.models.*
|
||||
import com.tangem.domain.feedback.repository.FeedbackRepository
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.io.File
|
||||
|
|
@ -23,23 +22,20 @@ import java.io.File
|
|||
/**
|
||||
* Implementation of [FeedbackRepository]
|
||||
*
|
||||
* @property appLogsStore app logs store
|
||||
* @property userWalletsListManager user wallets list manager
|
||||
* @property userWalletsListRepository user wallets repository
|
||||
* @property walletManagersStore wallet managers store
|
||||
* @property emailSender email sender
|
||||
* @property appVersionProvider app version provider
|
||||
* @property appLogsStore app logs store
|
||||
* @property userWalletsListRepository repository for getting user wallets
|
||||
* @property walletManagersStore wallet managers store
|
||||
* @property emailSender email sender
|
||||
* @property appInfoProvider app info provider
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultFeedbackRepository(
|
||||
private val appLogsStore: AppLogsStore,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val walletManagersStore: WalletManagersStore,
|
||||
private val emailSender: EmailSender,
|
||||
private val appVersionProvider: AppVersionProvider,
|
||||
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
private val appInfoProvider: AppInfoProvider,
|
||||
) : FeedbackRepository {
|
||||
|
||||
private val blockchainsErrors = MutableStateFlow<Map<UserWalletId, BlockchainErrorInfo>>(emptyMap())
|
||||
|
|
@ -68,30 +64,26 @@ internal class DefaultFeedbackRepository(
|
|||
.map(BlockchainInfoConverter::convert)
|
||||
}
|
||||
|
||||
override suspend fun getBlockchainInfo(
|
||||
userWalletId: UserWalletId,
|
||||
blockchainId: String,
|
||||
derivationPath: String?,
|
||||
): BlockchainInfo? {
|
||||
override suspend fun getBlockchainInfo(userWalletId: UserWalletId, networkId: Network.ID): BlockchainInfo? {
|
||||
return walletManagersStore
|
||||
.getSyncOrNull(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = Blockchain.fromId(blockchainId),
|
||||
derivationPath = derivationPath,
|
||||
blockchain = networkId.toBlockchain(),
|
||||
derivationPath = networkId.derivationPath.value,
|
||||
)
|
||||
?.let(BlockchainInfoConverter::convert)
|
||||
}
|
||||
|
||||
override fun getPhoneInfo(): PhoneInfo {
|
||||
return PhoneInfo(
|
||||
phoneModel = Build.MODEL,
|
||||
osVersion = Build.VERSION.SDK_INT.toString(),
|
||||
appVersion = appVersionProvider.versionName,
|
||||
phoneModel = appInfoProvider.device,
|
||||
osVersion = appInfoProvider.sdkVersion.toString(),
|
||||
appVersion = appInfoProvider.appVersion,
|
||||
)
|
||||
}
|
||||
|
||||
override fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) {
|
||||
val userWallet = getSelectedWalletUseCase.sync().getOrNull() ?: error("UserWallet is not selected")
|
||||
val userWallet = userWalletsListRepository.selectedUserWallet.value ?: error("UserWallet is not selected")
|
||||
|
||||
blockchainsErrors.update { map ->
|
||||
map.toMutableMap().apply {
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
|||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.feedback.repository.FeedbackFeatureToggles
|
||||
import com.tangem.domain.feedback.repository.FeedbackRepository
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -28,16 +27,14 @@ internal object FeedbackModule {
|
|||
userWalletsListRepository: UserWalletsListRepository,
|
||||
walletManagersStore: WalletManagersStore,
|
||||
emailSender: EmailSender,
|
||||
appVersionProvider: AppVersionProvider,
|
||||
getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
appInfoProvider: AppInfoProvider,
|
||||
): FeedbackRepository {
|
||||
return DefaultFeedbackRepository(
|
||||
appLogsStore = appLogsStore,
|
||||
walletManagersStore = walletManagersStore,
|
||||
emailSender = emailSender,
|
||||
appVersionProvider = appVersionProvider,
|
||||
appInfoProvider = appInfoProvider,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
getSelectedWalletUseCase = getSelectedWalletUseCase,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ internal class DefaultCustomTokensRepository(
|
|||
|
||||
val response = tangemTechApi.getCoins(
|
||||
contractAddress = contractAddress,
|
||||
networkIds = network.backendId,
|
||||
networkIds = network.rawId,
|
||||
active = true,
|
||||
).getOrThrow()
|
||||
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ internal class DefaultMarketsTokenRepository(
|
|||
|
||||
override suspend fun createCryptoCurrency(
|
||||
userWalletId: UserWalletId,
|
||||
token: TokenMarketParams,
|
||||
token: RawMarketToken,
|
||||
network: TokenMarketInfo.Network,
|
||||
accountIndex: DerivationIndex?,
|
||||
): CryptoCurrency? {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ dependencies {
|
|||
|
||||
// region Project - Data
|
||||
implementation(projects.data.common)
|
||||
implementation(projects.data.dynamicAddresses)
|
||||
// endregion
|
||||
|
||||
// region Project - Domain
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>MultilineLambdaItParameter:CommonNetworkStatusFetcher.kt$CommonNetworkStatusFetcher${ Timber.e("Failed to fetch network status for $userWalletId [${network.rawId}]: $it") networksStatusesStore.setSourceAsOnlyCache(userWalletId = userWalletId, network = network) }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultMultiNetworkStatusFetcher.kt$DefaultMultiNetworkStatusFetcher${ networksStatusesStore.setSourceAsOnlyCache( userWalletId = params.userWalletId, networks = params.networks, ) raise(it) }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultNetworksRepository.kt$DefaultNetworksRepository${ Timber.e(it, "Unable to create wallet currencies") return emptyList() }</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultNetworksRepository.kt$DefaultNetworksRepository${ Timber.e(it, "Unable to create wallet currencies") return@withContext }</ID>
|
||||
<ID>MultilineLambdaItParameter:NetworkAmountsConverter.kt$NetworkAmountsConverter${ val amount = it.value as? NetworkStatus.Amount.Loaded ?: return@mapNotNull null CurrencyAmount( id = currencyIdConverter.convertBack(value = it.key), amount = amount.value, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:NetworkAmountsConverter.kt$NetworkAmountsConverter${ val currencyId = currencyIdConverter.convert(value = it.id) val amount = NetworkStatus.Amount.Loaded(value = it.amount) currencyId to amount }</ID>
|
||||
<ID>MultilineLambdaItParameter:NetworkStatusSupplierModule.kt$NetworkStatusSupplierModule.<no name provided>${ "single_network_status_${it.userWalletId.stringValue}_${it.network.rawId}_" + it.network.derivationPath.value }</ID>
|
||||
<ID>MultilineLambdaItParameter:NetworkYieldSupplyStatusConverter.kt$NetworkYieldSupplyStatusConverter${ val id = currencyIdConverter.convert(value = it.id) val status = YieldSupplyStatus( isActive = it.isActive, isInitialized = it.isInitialized, isAllowedToSpend = it.isAllowedToSpend, effectiveProtocolBalance = it.effectiveProtocolBalance, ) id to status }</ID>
|
||||
<ID>SuspendFunSwallowedCancellation:DefaultNetworksRepository.kt$DefaultNetworksRepository$runCatching</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -12,33 +12,33 @@ private typealias AmountsDomainModel = Map<CryptoCurrency.ID, NetworkStatus.Amou
|
|||
/**
|
||||
* Converter from [AmountsDataModel] to [AmountsDomainModel] and vice versa
|
||||
*
|
||||
* @param rawNetworkId the raw network ID associated with the currency
|
||||
* @param blockchainId the blockchain ID associated with the network (e.g. `Blockchain.id`)
|
||||
* @param derivationPath the derivation path used for the network
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class NetworkAmountsConverter(
|
||||
rawNetworkId: String,
|
||||
blockchainId: String,
|
||||
derivationPath: Network.DerivationPath,
|
||||
) : TwoWayConverter<AmountsDataModel, AmountsDomainModel> {
|
||||
|
||||
private val currencyIdConverter = CurrencyIdConverter(rawNetworkId, derivationPath)
|
||||
private val currencyIdConverter = NetworkCurrencyIdConverter(blockchainId, derivationPath)
|
||||
|
||||
override fun convert(value: AmountsDataModel): AmountsDomainModel {
|
||||
return value.associate {
|
||||
val currencyId = currencyIdConverter.convert(value = it.id)
|
||||
val amount = NetworkStatus.Amount.Loaded(value = it.amount)
|
||||
return value.associate { currencyAmount ->
|
||||
val currencyId = currencyIdConverter.convert(value = currencyAmount.id)
|
||||
val amount = NetworkStatus.Amount.Loaded(value = currencyAmount.amount)
|
||||
|
||||
currencyId to amount
|
||||
}
|
||||
}
|
||||
|
||||
override fun convertBack(value: AmountsDomainModel): AmountsDataModel {
|
||||
return value.mapNotNull {
|
||||
val amount = it.value as? NetworkStatus.Amount.Loaded ?: return@mapNotNull null
|
||||
return value.mapNotNull { (currencyId, networkAmount) ->
|
||||
val amount = networkAmount as? NetworkStatus.Amount.Loaded ?: return@mapNotNull null
|
||||
|
||||
CurrencyAmount(
|
||||
id = currencyIdConverter.convertBack(value = it.key),
|
||||
id = currencyIdConverter.convertBack(value = currencyId),
|
||||
amount = amount.value,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.data.networks.converters
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.blockchainsdk.utils.toCoinId
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM.CurrencyId
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM.CurrencyId.Companion.CONTRACT_ADDRESS_DELIMITER
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
@ -12,16 +14,22 @@ import com.tangem.domain.models.currency.CryptoCurrency.ID.Suffix as CurrencyIdS
|
|||
/**
|
||||
* Converts between [CurrencyId] and [CryptoCurrency.ID].
|
||||
*
|
||||
* @property rawNetworkId the raw network ID associated with the currency
|
||||
* @property blockchainId the blockchain ID associated with the currency
|
||||
* @property derivationPath the derivation path used for the network
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class CurrencyIdConverter(
|
||||
private val rawNetworkId: String,
|
||||
internal class NetworkCurrencyIdConverter(
|
||||
private val blockchainId: String,
|
||||
private val derivationPath: Network.DerivationPath,
|
||||
) : TwoWayConverter<CurrencyId, CryptoCurrency.ID> {
|
||||
|
||||
// Cache stores blockchainId in legacy format (e.g. "BTC"), but runtime
|
||||
// CryptoCurrency.ID expects the new network rawId (e.g. "bitcoin") matching
|
||||
// Network.rawId built from Blockchain.toNetworkId(). Convert once on construction
|
||||
// so that IDs reconstructed from cache match those built at runtime.
|
||||
private val networkRawId: String = Blockchain.fromId(blockchainId).toNetworkId()
|
||||
|
||||
override fun convert(value: CurrencyId): CryptoCurrency.ID {
|
||||
val suffixParts = value.value.split(CONTRACT_ADDRESS_DELIMITER)
|
||||
|
||||
|
|
@ -31,7 +39,7 @@ internal class CurrencyIdConverter(
|
|||
return if (contractAddress.isNullOrBlank()) {
|
||||
getCoinId(
|
||||
coinId = rawId.takeUnless { it.isNullOrBlank() }
|
||||
?: error("Coin id is null for $rawNetworkId with $derivationPath"),
|
||||
?: error("Coin id is null for $blockchainId with $derivationPath"),
|
||||
)
|
||||
} else {
|
||||
getTokenId(
|
||||
|
|
@ -43,14 +51,12 @@ internal class CurrencyIdConverter(
|
|||
|
||||
override fun convertBack(value: CryptoCurrency.ID): CurrencyId {
|
||||
return if (value.isCoin) {
|
||||
CurrencyId.createCoinId(
|
||||
coinId = Blockchain.fromId(value.rawNetworkId).toCoinId(),
|
||||
)
|
||||
CurrencyId.createCoinId(coinId = value.toBlockchain().toCoinId())
|
||||
} else {
|
||||
CurrencyId.createTokenId(
|
||||
rawTokenId = value.rawCurrencyId?.value,
|
||||
contractAddress = requireNotNull(value.contractAddress) {
|
||||
"Token contractAddress is null for token id: $this"
|
||||
"Token contractAddress is null for token id: $value"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -82,17 +88,17 @@ internal class CurrencyIdConverter(
|
|||
return when (derivationPath) {
|
||||
is Network.DerivationPath.Card -> {
|
||||
CryptoCurrency.ID.Body.NetworkIdWithDerivationPath(
|
||||
rawId = rawNetworkId,
|
||||
rawId = networkRawId,
|
||||
derivationPath = derivationPath.value,
|
||||
)
|
||||
}
|
||||
is Network.DerivationPath.Custom -> {
|
||||
CryptoCurrency.ID.Body.NetworkIdWithDerivationPath(
|
||||
rawId = rawNetworkId,
|
||||
rawId = networkRawId,
|
||||
derivationPath = derivationPath.value,
|
||||
)
|
||||
}
|
||||
is Network.DerivationPath.None -> CryptoCurrency.ID.Body.NetworkId(rawNetworkId)
|
||||
is Network.DerivationPath.None -> CryptoCurrency.ID.Body.NetworkId(networkRawId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.data.networks.converters
|
||||
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
|
@ -15,17 +16,18 @@ internal object NetworkStatusDataModelConverter : Converter<NetworkStatus, Netwo
|
|||
return when (val status = value.value) {
|
||||
is NetworkStatus.Verified -> {
|
||||
val address = NetworkAddressConverter.convertBack(value = status.address)
|
||||
val blockchainId = value.network.toBlockchain().id
|
||||
val amountsConverter = NetworkAmountsConverter(
|
||||
rawNetworkId = value.network.rawId,
|
||||
blockchainId = blockchainId,
|
||||
derivationPath = value.network.derivationPath,
|
||||
)
|
||||
val yieldSupplyStatusConverter = NetworkYieldSupplyStatusConverter(
|
||||
rawNetworkId = value.network.rawId,
|
||||
blockchainId = blockchainId,
|
||||
derivationPath = value.network.derivationPath,
|
||||
)
|
||||
|
||||
NetworkStatusDM.Verified(
|
||||
networkId = NetworkStatusDM.ID(value = value.network.rawId),
|
||||
networkId = NetworkStatusDM.ID(value = blockchainId),
|
||||
derivationPath = NetworkDerivationPathConverter.convertBack(value = value.network.derivationPath),
|
||||
selectedAddress = address.selectedAddress,
|
||||
availableAddresses = address.addresses,
|
||||
|
|
@ -35,9 +37,10 @@ internal object NetworkStatusDataModelConverter : Converter<NetworkStatus, Netwo
|
|||
}
|
||||
is NetworkStatus.NoAccount -> {
|
||||
val address = NetworkAddressConverter.convertBack(value = status.address)
|
||||
val blockchainId = value.network.toBlockchain().id
|
||||
|
||||
NetworkStatusDM.NoAccount(
|
||||
networkId = NetworkStatusDM.ID(value = value.network.rawId),
|
||||
networkId = NetworkStatusDM.ID(value = blockchainId),
|
||||
derivationPath = NetworkDerivationPathConverter.convertBack(value = value.network.derivationPath),
|
||||
selectedAddress = address.selectedAddress,
|
||||
availableAddresses = address.addresses,
|
||||
|
|
|
|||
|
|
@ -10,20 +10,20 @@ private typealias YieldSupplyStatusDataModel = List<NetworkStatusDM.YieldSupplyS
|
|||
private typealias YieldSupplyStatusDomainModel = Map<CryptoCurrency.ID, YieldSupplyStatus?>
|
||||
|
||||
internal class NetworkYieldSupplyStatusConverter(
|
||||
rawNetworkId: String,
|
||||
blockchainId: String,
|
||||
derivationPath: Network.DerivationPath,
|
||||
) : TwoWayConverter<YieldSupplyStatusDataModel, YieldSupplyStatusDomainModel> {
|
||||
|
||||
private val currencyIdConverter = CurrencyIdConverter(rawNetworkId, derivationPath)
|
||||
private val currencyIdConverter = NetworkCurrencyIdConverter(blockchainId, derivationPath)
|
||||
|
||||
override fun convert(value: YieldSupplyStatusDataModel): YieldSupplyStatusDomainModel {
|
||||
return value.associate {
|
||||
val id = currencyIdConverter.convert(value = it.id)
|
||||
return value.associate { yieldSupplyStatus ->
|
||||
val id = currencyIdConverter.convert(value = yieldSupplyStatus.id)
|
||||
val status = YieldSupplyStatus(
|
||||
isActive = it.isActive,
|
||||
isInitialized = it.isInitialized,
|
||||
isAllowedToSpend = it.isAllowedToSpend,
|
||||
effectiveProtocolBalance = it.effectiveProtocolBalance,
|
||||
isActive = yieldSupplyStatus.isActive,
|
||||
isInitialized = yieldSupplyStatus.isInitialized,
|
||||
isAllowedToSpend = yieldSupplyStatus.isAllowedToSpend,
|
||||
effectiveProtocolBalance = yieldSupplyStatus.effectiveProtocolBalance,
|
||||
)
|
||||
|
||||
id to status
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.data.networks.converters
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.data.networks.models.SimpleNetworkStatus
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM
|
||||
import com.tangem.domain.models.StatusSource
|
||||
|
|
@ -23,14 +25,15 @@ internal object SimpleNetworkStatusConverter : Converter<NetworkStatusDM, Simple
|
|||
)
|
||||
|
||||
val derivationPath = NetworkDerivationPathConverter.convert(value = value.derivationPath)
|
||||
val blockchainId = value.networkId.value
|
||||
|
||||
val amountsConverter = NetworkAmountsConverter(
|
||||
rawNetworkId = value.networkId.value,
|
||||
blockchainId = blockchainId,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
|
||||
val yieldSupplyStatusConverter = NetworkYieldSupplyStatusConverter(
|
||||
rawNetworkId = value.networkId.value,
|
||||
blockchainId = blockchainId,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
|
||||
|
|
@ -56,7 +59,7 @@ internal object SimpleNetworkStatusConverter : Converter<NetworkStatusDM, Simple
|
|||
|
||||
return SimpleNetworkStatus(
|
||||
id = Network.ID(
|
||||
value = value.networkId.value,
|
||||
value = Blockchain.fromId(blockchainId).toNetworkId(),
|
||||
derivationPath = NetworkDerivationPathConverter.convert(value = value.derivationPath),
|
||||
),
|
||||
value = status,
|
||||
|
|
|
|||
|
|
@ -17,21 +17,26 @@ internal object NetworkStatusSupplierModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideSingleNetworkStatusSupplier(factory: SingleNetworkStatusProducer.Factory): SingleNetworkStatusSupplier {
|
||||
return object : SingleNetworkStatusSupplier(
|
||||
return SingleNetworkStatusSupplier(
|
||||
factory = factory,
|
||||
keyCreator = {
|
||||
"single_network_status_${it.userWalletId.stringValue}_${it.network.rawId}_" +
|
||||
it.network.derivationPath.value
|
||||
keyCreator = { params ->
|
||||
listOf(
|
||||
"single_network_status",
|
||||
params.userWalletId.stringValue,
|
||||
params.network.rawId,
|
||||
params.network.derivationPath.value,
|
||||
)
|
||||
.joinToString(separator = "_")
|
||||
},
|
||||
) {}
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMultiNetworkStatusSupplier(factory: MultiNetworkStatusProducer.Factory): MultiNetworkStatusSupplier {
|
||||
return object : MultiNetworkStatusSupplier(
|
||||
return MultiNetworkStatusSupplier(
|
||||
factory = factory,
|
||||
keyCreator = { "multi_networks_statuses_${it.userWalletId.stringValue}" },
|
||||
) {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +43,7 @@ internal class CommonNetworkStatusFetcher @Inject constructor(
|
|||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
networkCurrencies: Set<CryptoCurrency>,
|
||||
xpub: String? = null,
|
||||
): Either<Throwable, Unit> {
|
||||
return Either.catchOn(dispatchers.default) {
|
||||
val result = withContext(dispatchers.io) {
|
||||
|
|
@ -52,6 +53,7 @@ internal class CommonNetworkStatusFetcher @Inject constructor(
|
|||
extraTokens = networkCurrencies
|
||||
.filterIsInstance<CryptoCurrency.Token>()
|
||||
.toSet(),
|
||||
xpub = xpub,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,16 +2,18 @@ package com.tangem.data.networks.multi
|
|||
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.ensure
|
||||
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
|
||||
import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher
|
||||
import com.tangem.data.networks.store.NetworksStatusesStore
|
||||
import com.tangem.data.networks.store.setSourceAsCache
|
||||
import com.tangem.data.networks.store.setSourceAsOnlyCache
|
||||
import com.tangem.data.dynamicaddresses.DynamicAddressesInitializer
|
||||
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
|
||||
import com.tangem.domain.core.utils.eitherOn
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
|
@ -27,11 +29,11 @@ import javax.inject.Inject
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
|
||||
private val networksStatusesStore: NetworksStatusesStore,
|
||||
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher,
|
||||
private val dynamicAddressesInitializer: DynamicAddressesInitializer,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MultiNetworkStatusFetcher {
|
||||
|
||||
|
|
@ -40,13 +42,21 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
|
|||
|
||||
val networksCurrencies = catch(
|
||||
block = { createNetworksCurrenciesMap(params) },
|
||||
catch = {
|
||||
catch = { error ->
|
||||
networksStatusesStore.setSourceAsOnlyCache(
|
||||
userWalletId = params.userWalletId,
|
||||
networks = params.networks,
|
||||
)
|
||||
|
||||
raise(it)
|
||||
raise(error)
|
||||
},
|
||||
)
|
||||
|
||||
val xpubByNetwork = catch(
|
||||
block = { dynamicAddressesInitializer.getXpubs(params.userWalletId, params.networks) },
|
||||
catch = { error ->
|
||||
TangemLogger.e("Failed to build XPUBs for restore", error)
|
||||
emptyMap()
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -58,6 +68,7 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
|
|||
userWalletId = params.userWalletId,
|
||||
network = network,
|
||||
networkCurrencies = networksCurrencies[network].orEmpty().toSet(),
|
||||
xpub = xpubByNetwork[network],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import com.tangem.domain.networks.repository.NetworksRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
|
@ -33,13 +34,12 @@ internal class DefaultNetworksRepository(
|
|||
|
||||
override suspend fun fetchPendingTransactions(userWalletId: UserWalletId, network: Network) {
|
||||
withContext(dispatchers.default) {
|
||||
val currencies = runCatching {
|
||||
val currencies = runSuspendCatching {
|
||||
cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network)
|
||||
}.getOrElse { error ->
|
||||
TangemLogger.e("Unable to create wallet currencies", error)
|
||||
return@withContext
|
||||
}
|
||||
.getOrElse { error ->
|
||||
TangemLogger.e("Unable to create wallet currencies", error)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
fetchPendingTransactions(userWalletId = userWalletId, network = network, currencies = currencies)
|
||||
}
|
||||
|
|
@ -49,34 +49,34 @@ internal class DefaultNetworksRepository(
|
|||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): List<CryptoCurrencyAddress> {
|
||||
return runCatching { cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) }
|
||||
.getOrElse { error ->
|
||||
TangemLogger.e("Unable to create wallet currencies", error)
|
||||
return emptyList()
|
||||
}
|
||||
.map { currency ->
|
||||
CryptoCurrencyAddress(
|
||||
cryptoCurrency = currency,
|
||||
address = getDefaultAddress(userWalletId, network).orEmpty(),
|
||||
)
|
||||
}
|
||||
return runSuspendCatching {
|
||||
cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network)
|
||||
}.getOrElse { error ->
|
||||
TangemLogger.e("Unable to create wallet currencies", error)
|
||||
return emptyList()
|
||||
}.map { currency ->
|
||||
CryptoCurrencyAddress(
|
||||
cryptoCurrency = currency,
|
||||
address = getDefaultAddress(userWalletId, network).orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getNetworkAddresses(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network.RawID,
|
||||
): List<CryptoCurrencyAddress> {
|
||||
return runCatching { cardCryptoCurrencyFactory.createByRawId(userWalletId = userWalletId, network = network) }
|
||||
.getOrElse { error ->
|
||||
TangemLogger.e("Unable to create wallet currencies", error)
|
||||
return emptyList()
|
||||
}
|
||||
.map { currency ->
|
||||
CryptoCurrencyAddress(
|
||||
cryptoCurrency = currency,
|
||||
address = getDefaultAddress(userWalletId, currency.network).orEmpty(),
|
||||
)
|
||||
}
|
||||
return runSuspendCatching {
|
||||
cardCryptoCurrencyFactory.createByRawId(userWalletId = userWalletId, network = network)
|
||||
}.getOrElse { error ->
|
||||
TangemLogger.e("Unable to create wallet currencies", error)
|
||||
return emptyList()
|
||||
}.map { currency ->
|
||||
CryptoCurrencyAddress(
|
||||
cryptoCurrency = currency,
|
||||
address = getDefaultAddress(userWalletId, currency.network).orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getDefaultAddress(userWalletId: UserWalletId, network: Network): String? {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.data.networks.store
|
|||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.data.networks.converters.NetworkStatusDataModelConverter
|
||||
import com.tangem.data.networks.converters.SimpleNetworkStatusConverter
|
||||
import com.tangem.data.networks.models.SimpleNetworkStatus
|
||||
|
|
@ -27,15 +28,15 @@ internal typealias WalletIdWithStatusDM = Map<String, Set<NetworkStatusDM>>
|
|||
* Default implementation of [NetworksStatusesStore]
|
||||
*
|
||||
* @param context context
|
||||
* @param scope app coroutine scope
|
||||
* @property runtimeStore runtime store
|
||||
* @property persistenceDataStore persistence store
|
||||
* @param dispatchers dispatchers
|
||||
*/
|
||||
internal class DefaultNetworksStatusesStore(
|
||||
context: Context,
|
||||
scope: AppCoroutineScope,
|
||||
private val runtimeStore: RuntimeSharedStore<WalletIdWithSimpleStatus>,
|
||||
private val persistenceDataStore: DataStore<WalletIdWithStatusDM>,
|
||||
private val scope: AppCoroutineScope,
|
||||
) : NetworksStatusesStore {
|
||||
|
||||
init {
|
||||
|
|
@ -112,7 +113,8 @@ internal class DefaultNetworksStatusesStore(
|
|||
storedStatuses.toMutableMap().apply {
|
||||
val updatedValues = this[userWalletId.stringValue].orEmpty().filterNot {
|
||||
networks.any { network ->
|
||||
it.networkId.value == network.rawId && it.derivationPath.value == network.derivationPath.value
|
||||
it.networkId.value == network.toBlockchain().id &&
|
||||
it.derivationPath.value == network.derivationPath.value
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,10 +16,14 @@ import java.math.BigDecimal
|
|||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class NetworkAmountsConverterTest {
|
||||
|
||||
private val rawNetworkId = "ETH"
|
||||
// Cache stores the SDK-level Blockchain.id (legacy format, e.g. "ETH").
|
||||
// The converter normalizes it to the canonical network rawId ("ethereum") via
|
||||
// Blockchain.fromId(...).toNetworkId() so that resulting CryptoCurrency.IDs match those
|
||||
// built at runtime from Network.rawId.
|
||||
private val blockchainId = "ETH"
|
||||
private val derivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0")
|
||||
private val derivationPathHashCode = "-1843072795"
|
||||
private val converter = NetworkAmountsConverter(rawNetworkId = rawNetworkId, derivationPath = derivationPath)
|
||||
private val converter = NetworkAmountsConverter(blockchainId = blockchainId, derivationPath = derivationPath)
|
||||
|
||||
@Test
|
||||
fun convert() {
|
||||
|
|
@ -47,12 +51,12 @@ internal class NetworkAmountsConverterTest {
|
|||
|
||||
// Assert
|
||||
val expected = mapOf(
|
||||
ID.fromValue("coin⟨ETH→$derivationPathHashCode⟩ethereum") to Loaded(value = BigDecimal.ONE),
|
||||
ID.fromValue("coin⟨ethereum→$derivationPathHashCode⟩ethereum") to Loaded(value = BigDecimal.ONE),
|
||||
ID.fromValue(
|
||||
value = "token⟨ETH→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
value = "token⟨ethereum→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
) to Loaded(value = BigDecimal.ZERO),
|
||||
ID.fromValue(
|
||||
value = "token⟨ETH→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
value = "token⟨ethereum→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
) to Loaded(value = BigDecimal.TEN),
|
||||
)
|
||||
|
||||
|
|
@ -63,12 +67,12 @@ internal class NetworkAmountsConverterTest {
|
|||
fun convertBack() {
|
||||
// Arrange
|
||||
val value = mapOf(
|
||||
ID.fromValue("coin⟨ETH→$derivationPathHashCode⟩ethereum") to Loaded(value = BigDecimal.ONE),
|
||||
ID.fromValue("coin⟨ethereum→$derivationPathHashCode⟩ethereum") to Loaded(value = BigDecimal.ONE),
|
||||
ID.fromValue(
|
||||
value = "token⟨ETH→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
value = "token⟨ethereum→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
) to Loaded(value = BigDecimal.ZERO),
|
||||
ID.fromValue(
|
||||
value = "token⟨ETH→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
value = "token⟨ethereum→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
) to Loaded(value = BigDecimal.TEN),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
|
|
@ -13,12 +14,16 @@ import org.junit.jupiter.params.ParameterizedTest
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class CurrencyIdConverterTest {
|
||||
class NetworkCurrencyIdConverterTest {
|
||||
|
||||
private val rawNetworkId = "ETH"
|
||||
// Legacy SDK format stored in cache (see NetworkStatusDataModelConverter:
|
||||
// `value.network.toBlockchain().id`). Runtime CryptoCurrency.ID expects the canonical
|
||||
// network rawId ("ethereum"), so the converter normalizes via Blockchain.fromId(...).toNetworkId().
|
||||
private val blockchainId = "ETH"
|
||||
private val canonicalNetworkRawId = "ethereum"
|
||||
private val derivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0")
|
||||
private val derivationPathHashCode = "-1843072795"
|
||||
private val converter = CurrencyIdConverter(rawNetworkId = rawNetworkId, derivationPath = derivationPath)
|
||||
private val converter = NetworkCurrencyIdConverter(blockchainId = blockchainId, derivationPath = derivationPath)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
|
|
@ -48,19 +53,19 @@ class CurrencyIdConverterTest {
|
|||
ConvertModel(
|
||||
value = CurrencyId.createCoinId("ethereum"),
|
||||
expected = Result.success(
|
||||
CryptoCurrency.ID.fromValue(value = "coin⟨ETH→$derivationPathHashCode⟩ethereum"),
|
||||
CryptoCurrency.ID.fromValue(value = "coin⟨ethereum→$derivationPathHashCode⟩ethereum"),
|
||||
),
|
||||
),
|
||||
ConvertModel(
|
||||
value = CurrencyId.createCoinId(""),
|
||||
expected = Result.failure(
|
||||
IllegalStateException("Coin id is null for $rawNetworkId with $derivationPath"),
|
||||
IllegalStateException("Coin id is null for $blockchainId with $derivationPath"),
|
||||
),
|
||||
),
|
||||
ConvertModel(
|
||||
value = CurrencyId.createCoinId(" "),
|
||||
expected = Result.failure(
|
||||
IllegalStateException("Coin id is null for $rawNetworkId with $derivationPath"),
|
||||
IllegalStateException("Coin id is null for $blockchainId with $derivationPath"),
|
||||
),
|
||||
),
|
||||
// create token id
|
||||
|
|
@ -71,7 +76,7 @@ class CurrencyIdConverterTest {
|
|||
),
|
||||
expected = Result.success(
|
||||
CryptoCurrency.ID.fromValue(
|
||||
value = "token⟨ETH→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
value = "token⟨ethereum→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -82,7 +87,7 @@ class CurrencyIdConverterTest {
|
|||
),
|
||||
expected = Result.success(
|
||||
CryptoCurrency.ID.fromValue(
|
||||
value = "token⟨ETH→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
value = "token⟨ethereum→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -93,7 +98,7 @@ class CurrencyIdConverterTest {
|
|||
),
|
||||
expected = Result.success(
|
||||
CryptoCurrency.ID.fromValue(
|
||||
value = "token⟨ETH→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
value = "token⟨ethereum→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -104,7 +109,7 @@ class CurrencyIdConverterTest {
|
|||
),
|
||||
expected = Result.success(
|
||||
CryptoCurrency.ID.fromValue(
|
||||
value = "token⟨ETH→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
value = "token⟨ethereum→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -114,7 +119,7 @@ class CurrencyIdConverterTest {
|
|||
contractAddress = "",
|
||||
),
|
||||
expected = Result.success(
|
||||
CryptoCurrency.ID.fromValue(value = "coin⟨ETH→$derivationPathHashCode⟩usdt"),
|
||||
CryptoCurrency.ID.fromValue(value = "coin⟨ethereum→$derivationPathHashCode⟩usdt"),
|
||||
),
|
||||
),
|
||||
ConvertModel(
|
||||
|
|
@ -123,7 +128,7 @@ class CurrencyIdConverterTest {
|
|||
contractAddress = " ",
|
||||
),
|
||||
expected = Result.success(
|
||||
CryptoCurrency.ID.fromValue(value = "coin⟨ETH→$derivationPathHashCode⟩usdt"),
|
||||
CryptoCurrency.ID.fromValue(value = "coin⟨ethereum→$derivationPathHashCode⟩usdt"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -154,14 +159,14 @@ class CurrencyIdConverterTest {
|
|||
|
||||
private fun provideTestModels(): Collection<ConvertBackModel> = listOf(
|
||||
ConvertBackModel(
|
||||
value = CryptoCurrency.ID.fromValue("coin⟨ETH→$derivationPathHashCode⟩ethereum"),
|
||||
value = CryptoCurrency.ID.fromValue("coin⟨ethereum→$derivationPathHashCode⟩ethereum"),
|
||||
expected = Result.success(
|
||||
CurrencyId.createCoinId("ethereum"),
|
||||
),
|
||||
),
|
||||
ConvertBackModel(
|
||||
value = CryptoCurrency.ID.fromValue(
|
||||
value = "token⟨ETH→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
value = "token⟨ethereum→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
),
|
||||
expected = Result.success(
|
||||
CurrencyId.createTokenId(
|
||||
|
|
@ -172,7 +177,7 @@ class CurrencyIdConverterTest {
|
|||
),
|
||||
ConvertBackModel(
|
||||
value = CryptoCurrency.ID.fromValue(
|
||||
value = "token⟨ETH→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
value = "token⟨ethereum→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
),
|
||||
expected = Result.success(
|
||||
CurrencyId.createTokenId(
|
||||
|
|
@ -184,6 +189,54 @@ class CurrencyIdConverterTest {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression coverage for [REDACTED_TASK_KEY]. Cache stores `blockchainId` in the legacy SDK format
|
||||
* (`Blockchain.id`, e.g. "ETH"), but runtime [CryptoCurrency.ID] is built using the canonical
|
||||
* network rawId (`Blockchain.toNetworkId()`, e.g. "ethereum"). The converter must bridge the
|
||||
* two formats so that IDs reconstructed from cache equal those built at runtime — otherwise
|
||||
* `NetworkStatus.Verified.amounts[currency.id]` returns null and the wallet shimmer never clears.
|
||||
*/
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class LegacyBlockchainIdNormalization {
|
||||
|
||||
@Test
|
||||
fun `convert with legacy ETH blockchainId produces id with canonical ethereum rawId`() {
|
||||
val cached = CurrencyId.createCoinId("ethereum")
|
||||
|
||||
val result = converter.convert(cached)
|
||||
|
||||
Truth.assertThat(result)
|
||||
.isEqualTo(CryptoCurrency.ID.fromValue("coin⟨$canonicalNetworkRawId→$derivationPathHashCode⟩ethereum"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convert with legacy BTC blockchainId produces id with canonical bitcoin rawId`() {
|
||||
val btcDerivationPath = Network.DerivationPath.Card(value = "m/44'/0'/0'/0/0")
|
||||
val btcDerivationHash = btcDerivationPath.value.hashCode()
|
||||
val btcConverter = NetworkCurrencyIdConverter(
|
||||
blockchainId = "BTC",
|
||||
derivationPath = btcDerivationPath,
|
||||
)
|
||||
val cached = CurrencyId.createCoinId("bitcoin")
|
||||
|
||||
val result = btcConverter.convert(cached)
|
||||
|
||||
Truth.assertThat(result)
|
||||
.isEqualTo(CryptoCurrency.ID.fromValue("coin⟨bitcoin→$btcDerivationHash⟩bitcoin"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convert and convertBack roundtrip preserves CurrencyId`() {
|
||||
val cached = CurrencyId.createCoinId("ethereum")
|
||||
|
||||
val runtimeId = converter.convert(cached)
|
||||
val roundTrip = converter.convertBack(runtimeId)
|
||||
|
||||
Truth.assertThat(roundTrip).isEqualTo(cached)
|
||||
}
|
||||
}
|
||||
|
||||
data class ConvertModel(val value: CurrencyId, val expected: Result<CryptoCurrency.ID>)
|
||||
|
||||
data class ConvertBackModel(val value: CryptoCurrency.ID, val expected: Result<CurrencyId>)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.data.networks.converters
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM.*
|
||||
|
|
@ -49,7 +50,7 @@ internal class NetworkStatusDataModelConverterTest {
|
|||
),
|
||||
),
|
||||
amounts = mapOf(
|
||||
ID.fromValue(value = "coin⟨ETH→0⟩ethereum") to Amount.Loaded(value = BigDecimal.ZERO),
|
||||
ID.fromValue(value = "coin⟨ethereum→0⟩ethereum") to Amount.Loaded(value = BigDecimal.ZERO),
|
||||
ID(
|
||||
prefix = Prefix.COIN_PREFIX,
|
||||
body = Body.NetworkId(rawId = "BTC"),
|
||||
|
|
@ -74,7 +75,7 @@ internal class NetworkStatusDataModelConverterTest {
|
|||
),
|
||||
),
|
||||
expected = Verified(
|
||||
networkId = ID(network.rawId),
|
||||
networkId = ID(network.toBlockchain().id),
|
||||
derivationPath = DerivationPath(
|
||||
value = "",
|
||||
type = DerivationPath.Type.NONE,
|
||||
|
|
@ -119,7 +120,7 @@ internal class NetworkStatusDataModelConverterTest {
|
|||
),
|
||||
),
|
||||
expected = NoAccount(
|
||||
networkId = ID(network.rawId),
|
||||
networkId = ID(network.toBlockchain().id),
|
||||
derivationPath = DerivationPath(
|
||||
value = "",
|
||||
type = DerivationPath.Type.NONE,
|
||||
|
|
|
|||
|
|
@ -13,10 +13,14 @@ import java.math.BigDecimal
|
|||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class NetworkYieldSupplyStatusConverterTest {
|
||||
|
||||
private val rawNetworkId = "ETH"
|
||||
// Cache stores the SDK-level Blockchain.id (legacy format, e.g. "ETH").
|
||||
// The converter normalizes it to the canonical network rawId ("ethereum") via
|
||||
// Blockchain.fromId(...).toNetworkId() so that resulting CryptoCurrency.IDs match those
|
||||
// built at runtime from Network.rawId.
|
||||
private val blockchainId = "ETH"
|
||||
private val derivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0")
|
||||
private val derivationPathHashCode = "-1843072795"
|
||||
private val converter = NetworkYieldSupplyStatusConverter(rawNetworkId, derivationPath)
|
||||
private val converter = NetworkYieldSupplyStatusConverter(blockchainId, derivationPath)
|
||||
|
||||
private val domainStatus = YieldSupplyStatus(
|
||||
isActive = true,
|
||||
|
|
@ -38,8 +42,8 @@ internal class NetworkYieldSupplyStatusConverterTest {
|
|||
|
||||
// Assert
|
||||
val expected = mapOf(
|
||||
ID.fromValue("coin⟨ETH→$derivationPathHashCode⟩ethereum") to domainStatus,
|
||||
ID.fromValue("token⟨ETH→$derivationPathHashCode⟩usdt⚓0x1") to domainStatus,
|
||||
ID.fromValue("coin⟨ethereum→$derivationPathHashCode⟩ethereum") to domainStatus,
|
||||
ID.fromValue("token⟨ethereum→$derivationPathHashCode⟩usdt⚓0x1") to domainStatus,
|
||||
)
|
||||
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
|
@ -49,9 +53,9 @@ internal class NetworkYieldSupplyStatusConverterTest {
|
|||
fun convertBack() {
|
||||
// Arrange
|
||||
val value = mapOf(
|
||||
ID.fromValue("coin⟨ETH→$derivationPathHashCode⟩ethereum") to domainStatus,
|
||||
ID.fromValue("token⟨ETH→$derivationPathHashCode⟩usdt⚓0x1") to domainStatus,
|
||||
ID.fromValue("token⟨ETH→$derivationPathHashCode⟩usdc⚓0x1") to null,
|
||||
ID.fromValue("coin⟨ethereum→$derivationPathHashCode⟩ethereum") to domainStatus,
|
||||
ID.fromValue("token⟨ethereum→$derivationPathHashCode⟩usdt⚓0x1") to domainStatus,
|
||||
ID.fromValue("token⟨ethereum→$derivationPathHashCode⟩usdc⚓0x1") to null,
|
||||
)
|
||||
|
||||
// Act
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.data.networks.converters
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.data.networks.models.SimpleNetworkStatus
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM
|
||||
|
|
@ -48,7 +49,7 @@ internal class SimpleNetworkStatusConverterTest {
|
|||
// region Verified
|
||||
ConvertModel(
|
||||
value = Verified(
|
||||
networkId = ID(network.rawId),
|
||||
networkId = ID(network.toBlockchain().id),
|
||||
derivationPath = DerivationPath(
|
||||
value = "card",
|
||||
type = DerivationPath.Type.CARD,
|
||||
|
|
@ -95,12 +96,12 @@ internal class SimpleNetworkStatusConverterTest {
|
|||
),
|
||||
),
|
||||
amounts = mapOf(
|
||||
ID.fromValue("coin⟨ETH→3046160⟩ethereum") to Amount.Loaded(value = BigDecimal.ZERO),
|
||||
ID.fromValue("token⟨ETH→3046160⟩usdt⚓0x1") to Amount.Loaded(value = BigDecimal.ZERO),
|
||||
ID.fromValue("coin⟨ethereum→3046160⟩ethereum") to Amount.Loaded(value = BigDecimal.ZERO),
|
||||
ID.fromValue("token⟨ethereum→3046160⟩usdt⚓0x1") to Amount.Loaded(value = BigDecimal.ZERO),
|
||||
),
|
||||
pendingTransactions = emptyMap(),
|
||||
yieldSupplyStatuses = mapOf(
|
||||
ID.fromValue("coin⟨ETH→3046160⟩ethereum") to YieldSupplyStatus(
|
||||
ID.fromValue("coin⟨ethereum→3046160⟩ethereum") to YieldSupplyStatus(
|
||||
isActive = false,
|
||||
isInitialized = false,
|
||||
isAllowedToSpend = false,
|
||||
|
|
@ -116,7 +117,7 @@ internal class SimpleNetworkStatusConverterTest {
|
|||
// region NoAccount
|
||||
ConvertModel(
|
||||
value = NoAccount(
|
||||
networkId = ID(network.rawId),
|
||||
networkId = ID(network.toBlockchain().id),
|
||||
derivationPath = DerivationPath(
|
||||
value = "card",
|
||||
type = DerivationPath.Type.CARD,
|
||||
|
|
@ -168,7 +169,7 @@ internal class SimpleNetworkStatusConverterTest {
|
|||
// region Error
|
||||
ConvertModel(
|
||||
value = Verified(
|
||||
networkId = ID(network.rawId),
|
||||
networkId = ID(network.toBlockchain().id),
|
||||
derivationPath = DerivationPath(
|
||||
value = "card",
|
||||
type = DerivationPath.Type.CARD,
|
||||
|
|
@ -189,7 +190,7 @@ internal class SimpleNetworkStatusConverterTest {
|
|||
),
|
||||
ConvertModel(
|
||||
value = Verified(
|
||||
networkId = ID(network.rawId),
|
||||
networkId = ID(network.toBlockchain().id),
|
||||
derivationPath = DerivationPath(
|
||||
value = "card",
|
||||
type = DerivationPath.Type.CARD,
|
||||
|
|
@ -205,7 +206,7 @@ internal class SimpleNetworkStatusConverterTest {
|
|||
),
|
||||
ConvertModel(
|
||||
value = Verified(
|
||||
networkId = ID(network.rawId),
|
||||
networkId = ID(network.toBlockchain().id),
|
||||
derivationPath = DerivationPath(
|
||||
value = "card",
|
||||
type = DerivationPath.Type.CARD,
|
||||
|
|
@ -230,7 +231,7 @@ internal class SimpleNetworkStatusConverterTest {
|
|||
),
|
||||
ConvertModel(
|
||||
value = NoAccount(
|
||||
networkId = ID(network.rawId),
|
||||
networkId = ID(network.toBlockchain().id),
|
||||
derivationPath = DerivationPath(
|
||||
value = "card",
|
||||
type = DerivationPath.Type.CARD,
|
||||
|
|
@ -255,7 +256,7 @@ internal class SimpleNetworkStatusConverterTest {
|
|||
),
|
||||
ConvertModel(
|
||||
value = NoAccount(
|
||||
networkId = ID(network.rawId),
|
||||
networkId = ID(network.toBlockchain().id),
|
||||
derivationPath = DerivationPath(
|
||||
value = "card",
|
||||
type = DerivationPath.Type.CARD,
|
||||
|
|
@ -276,7 +277,7 @@ internal class SimpleNetworkStatusConverterTest {
|
|||
),
|
||||
ConvertModel(
|
||||
value = NoAccount(
|
||||
networkId = ID(network.rawId),
|
||||
networkId = ID(network.toBlockchain().id),
|
||||
derivationPath = DerivationPath(
|
||||
value = "card",
|
||||
type = DerivationPath.Type.CARD,
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ internal class CommonNetworkStatusFetcherTest {
|
|||
val userWalletId = UserWalletId("011")
|
||||
val network = cryptoCurrencyFactory.ethereum.network
|
||||
val extraTokens = setOf(
|
||||
cryptoCurrencyFactory.createToken(Blockchain.Ethereum) as CryptoCurrency.Token,
|
||||
cryptoCurrencyFactory.createToken(Blockchain.Ethereum),
|
||||
)
|
||||
val updateException = IllegalStateException()
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ internal class CommonNetworkStatusFetcherTest {
|
|||
val userWalletId = UserWalletId("011")
|
||||
val network = cryptoCurrencyFactory.ethereum.network
|
||||
val extraTokens = setOf(
|
||||
cryptoCurrencyFactory.createToken(Blockchain.Ethereum) as CryptoCurrency.Token,
|
||||
cryptoCurrencyFactory.createToken(Blockchain.Ethereum),
|
||||
)
|
||||
val updateResult = model.updateResult
|
||||
val status = model.status
|
||||
|
|
@ -143,15 +143,15 @@ internal class CommonNetworkStatusFetcherTest {
|
|||
it.copy(
|
||||
amounts = mapOf(
|
||||
CryptoCurrency.ID.fromValue(
|
||||
value = "token⟨ETH⟩NEVER-MIND⚓NEVER-MIND",
|
||||
value = "token⟨ethereum⟩NEVER-MIND⚓NEVER-MIND",
|
||||
) to NetworkStatus.Amount.NotFound,
|
||||
),
|
||||
pendingTransactions = mapOf(
|
||||
CryptoCurrency.ID.fromValue(value = "token⟨ETH⟩NEVER-MIND⚓NEVER-MIND") to emptySet(),
|
||||
CryptoCurrency.ID.fromValue(value = "token⟨ethereum⟩NEVER-MIND⚓NEVER-MIND") to emptySet(),
|
||||
),
|
||||
yieldSupplyStatuses = mapOf(
|
||||
CryptoCurrency.ID.fromValue(
|
||||
value = "token⟨ETH⟩NEVER-MIND⚓NEVER-MIND",
|
||||
value = "token⟨ethereum⟩NEVER-MIND⚓NEVER-MIND",
|
||||
) to null,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.data.networks.multi
|
|||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.data.dynamicaddresses.DynamicAddressesInitializer
|
||||
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
|
||||
import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher
|
||||
import com.tangem.data.networks.store.NetworksStatusesStore
|
||||
|
|
@ -27,17 +28,21 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
private val networksStatusesStore: NetworksStatusesStore = mockk(relaxUnitFun = true)
|
||||
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk()
|
||||
private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher = mockk()
|
||||
private val dynamicAddressesInitializer: DynamicAddressesInitializer = mockk()
|
||||
|
||||
private val fetcher = DefaultMultiNetworkStatusFetcher(
|
||||
networksStatusesStore = networksStatusesStore,
|
||||
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
|
||||
commonNetworkStatusFetcher = commonNetworkStatusFetcher,
|
||||
dynamicAddressesInitializer = dynamicAddressesInitializer,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(networksStatusesStore, cardCryptoCurrencyFactory, commonNetworkStatusFetcher)
|
||||
clearMocks(networksStatusesStore, cardCryptoCurrencyFactory, commonNetworkStatusFetcher, dynamicAddressesInitializer)
|
||||
// No dynamic addresses restore by default
|
||||
coEvery { dynamicAddressesInitializer.getXpubs(any(), any()) } returns emptyMap()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -62,6 +67,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
userWalletId = params.userWalletId,
|
||||
network = ethereum.network,
|
||||
networkCurrencies = setOf(ethereum),
|
||||
xpub = null,
|
||||
)
|
||||
} returns ethereumFetcherResult
|
||||
|
||||
|
|
@ -70,6 +76,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
userWalletId = params.userWalletId,
|
||||
network = cardano.network,
|
||||
networkCurrencies = setOf(cardano),
|
||||
xpub = null,
|
||||
)
|
||||
} returns cardanoFetcherResult
|
||||
|
||||
|
|
@ -87,11 +94,13 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
userWalletId = params.userWalletId,
|
||||
network = ethereum.network,
|
||||
networkCurrencies = setOf(ethereum),
|
||||
xpub = null,
|
||||
)
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = cardano.network,
|
||||
networkCurrencies = setOf(cardano),
|
||||
xpub = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -122,6 +131,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
userWalletId = params.userWalletId,
|
||||
network = ethereum.network,
|
||||
networkCurrencies = setOf(ethereum),
|
||||
xpub = null,
|
||||
)
|
||||
} returns ethereumFetcherResult
|
||||
|
||||
|
|
@ -130,6 +140,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
userWalletId = params.userWalletId,
|
||||
network = cardano.network,
|
||||
networkCurrencies = setOf(cardano),
|
||||
xpub = null,
|
||||
)
|
||||
} returns cardanoFetcherResult
|
||||
|
||||
|
|
@ -147,11 +158,13 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
userWalletId = params.userWalletId,
|
||||
network = ethereum.network,
|
||||
networkCurrencies = setOf(ethereum),
|
||||
xpub = null,
|
||||
)
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = cardano.network,
|
||||
networkCurrencies = setOf(cardano),
|
||||
xpub = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -182,6 +195,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
userWalletId = params.userWalletId,
|
||||
network = ethereum.network,
|
||||
networkCurrencies = setOf(ethereum),
|
||||
xpub = null,
|
||||
)
|
||||
} returns ethereumFetcherResult
|
||||
|
||||
|
|
@ -190,6 +204,7 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
userWalletId = params.userWalletId,
|
||||
network = cardano.network,
|
||||
networkCurrencies = setOf(cardano),
|
||||
xpub = null,
|
||||
)
|
||||
} returns cardanoFetcherResult
|
||||
|
||||
|
|
@ -207,11 +222,13 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
userWalletId = params.userWalletId,
|
||||
network = ethereum.network,
|
||||
networkCurrencies = setOf(ethereum),
|
||||
xpub = null,
|
||||
)
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = cardano.network,
|
||||
networkCurrencies = setOf(cardano),
|
||||
xpub = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -245,7 +262,220 @@ internal class DefaultMultiNetworkStatusFetcherTest {
|
|||
networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) { commonNetworkStatusFetcher.fetch(any(), any(), any()) }
|
||||
coVerify(inverse = true) { commonNetworkStatusFetcher.fetch(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch passes xpub to correct network and null to others`() = runTest {
|
||||
// Arrange
|
||||
val xpub = "xpub_test_eth"
|
||||
val params = setupTwoNetworkParams()
|
||||
coEvery { dynamicAddressesInitializer.getXpubs(params.userWalletId, params.networks) } returns mapOf(ethereum.network to xpub)
|
||||
|
||||
coEvery {
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = ethereum.network,
|
||||
networkCurrencies = setOf(ethereum),
|
||||
xpub = xpub,
|
||||
)
|
||||
} returns Either.Right(Unit)
|
||||
coEvery {
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = cardano.network,
|
||||
networkCurrencies = setOf(cardano),
|
||||
xpub = null,
|
||||
)
|
||||
} returns Either.Right(Unit)
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = Either.Right(Unit)
|
||||
assertEither(actual, expected)
|
||||
coVerify(exactly = 1) {
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = ethereum.network,
|
||||
networkCurrencies = setOf(ethereum),
|
||||
xpub = xpub,
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 1) {
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = cardano.network,
|
||||
networkCurrencies = setOf(cardano),
|
||||
xpub = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch continues with null xpub for all networks if getXpubs throws`() = runTest {
|
||||
// Arrange
|
||||
val params = setupTwoNetworkParams()
|
||||
coEvery {
|
||||
dynamicAddressesInitializer.getXpubs(params.userWalletId, params.networks)
|
||||
} throws RuntimeException("XPUB derivation failed")
|
||||
|
||||
val fetchResult = Either.Right(Unit)
|
||||
coEvery {
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = ethereum.network,
|
||||
networkCurrencies = setOf(ethereum),
|
||||
xpub = null,
|
||||
)
|
||||
} returns fetchResult
|
||||
coEvery {
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = cardano.network,
|
||||
networkCurrencies = setOf(cardano),
|
||||
xpub = null,
|
||||
)
|
||||
} returns fetchResult
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = Either.Right(Unit)
|
||||
assertEither(actual, expected)
|
||||
coVerify(exactly = 1) {
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = ethereum.network,
|
||||
networkCurrencies = setOf(ethereum),
|
||||
xpub = null,
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 1) {
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = cardano.network,
|
||||
networkCurrencies = setOf(cardano),
|
||||
xpub = null,
|
||||
)
|
||||
}
|
||||
// getXpubs failure must not degrade network status to OnlyCache
|
||||
coVerify(inverse = true) { networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch passes xpubs to all networks returned by getXpubs`() = runTest {
|
||||
// Arrange
|
||||
val ethXpub = "xpub_eth"
|
||||
val adaXpub = "xpub_ada"
|
||||
val params = setupTwoNetworkParams()
|
||||
coEvery {
|
||||
dynamicAddressesInitializer.getXpubs(params.userWalletId, params.networks)
|
||||
} returns mapOf(ethereum.network to ethXpub, cardano.network to adaXpub)
|
||||
|
||||
coEvery {
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = ethereum.network,
|
||||
networkCurrencies = setOf(ethereum),
|
||||
xpub = ethXpub,
|
||||
)
|
||||
} returns Either.Right(Unit)
|
||||
coEvery {
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = cardano.network,
|
||||
networkCurrencies = setOf(cardano),
|
||||
xpub = adaXpub,
|
||||
)
|
||||
} returns Either.Right(Unit)
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = Either.Right(Unit)
|
||||
assertEither(actual, expected)
|
||||
coVerify(exactly = 1) {
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = ethereum.network,
|
||||
networkCurrencies = setOf(ethereum),
|
||||
xpub = ethXpub,
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 1) {
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = cardano.network,
|
||||
networkCurrencies = setOf(cardano),
|
||||
xpub = adaXpub,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch calls getXpubs with exactly the networks from params`() = runTest {
|
||||
// Arrange
|
||||
val params = setupTwoNetworkParams()
|
||||
coEvery {
|
||||
commonNetworkStatusFetcher.fetch(userWalletId = any(), network = any(), networkCurrencies = any(), xpub = any())
|
||||
} returns Either.Right(Unit)
|
||||
|
||||
// Act
|
||||
fetcher(params)
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { dynamicAddressesInitializer.getXpubs(params.userWalletId, params.networks) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch failure if network fetch fails when xpub is provided`() = runTest {
|
||||
// Arrange
|
||||
val xpub = "xpub_eth"
|
||||
val params = setupTwoNetworkParams()
|
||||
coEvery {
|
||||
dynamicAddressesInitializer.getXpubs(params.userWalletId, params.networks)
|
||||
} returns mapOf(ethereum.network to xpub)
|
||||
|
||||
val fetchFailure = Either.Left(IllegalStateException())
|
||||
coEvery {
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = ethereum.network,
|
||||
networkCurrencies = setOf(ethereum),
|
||||
xpub = xpub,
|
||||
)
|
||||
} returns fetchFailure
|
||||
coEvery {
|
||||
commonNetworkStatusFetcher.fetch(
|
||||
userWalletId = params.userWalletId,
|
||||
network = cardano.network,
|
||||
networkCurrencies = setOf(cardano),
|
||||
xpub = null,
|
||||
)
|
||||
} returns Either.Right(Unit)
|
||||
|
||||
// Act
|
||||
val actual = fetcher(params)
|
||||
|
||||
// Assert
|
||||
val expected = Either.Left(IllegalStateException("Failed to fetch network statuses"))
|
||||
assertEither(actual, expected)
|
||||
}
|
||||
|
||||
private fun setupTwoNetworkParams(): MultiNetworkStatusFetcher.Params {
|
||||
val params = MultiNetworkStatusFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
networks = setOf(ethereum.network, cardano.network),
|
||||
)
|
||||
coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.networks) } returns mapOf(
|
||||
ethereum.network to listOf(ethereum),
|
||||
cardano.network to listOf(cardano),
|
||||
)
|
||||
return params
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import com.tangem.domain.news.repository.NewsRepository
|
|||
import com.tangem.pagination.*
|
||||
import com.tangem.pagination.exception.EndOfPaginationException
|
||||
import com.tangem.pagination.fetcher.BatchFetcher
|
||||
import com.tangem.utils.SupportedLanguages
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
|
@ -47,6 +48,9 @@ internal class DefaultNewsRepository(
|
|||
private val newsErrorResolver: NewsErrorResolver,
|
||||
) : NewsRepository {
|
||||
|
||||
private val language: String
|
||||
get() = SupportedLanguages.getCurrentSupportedLanguageCode()
|
||||
|
||||
override fun getNewsListBatchFlow(context: NewsListBatchingContext, batchSize: Int): NewsListBatchFlow {
|
||||
val newsBatchFlow = BatchListSource(
|
||||
fetchDispatcher = dispatchers.io,
|
||||
|
|
@ -63,7 +67,7 @@ internal class DefaultNewsRepository(
|
|||
val items = newsApi.getNews(
|
||||
page = FIRST_PAGE,
|
||||
limit = limit,
|
||||
language = config.language,
|
||||
language = language,
|
||||
snapshot = config.snapshot,
|
||||
tokenIds = config.tokenIds.takeIf { it.isNotEmpty() },
|
||||
categoryIds = config.categoryIds.takeIf { it.isNotEmpty() },
|
||||
|
|
@ -105,12 +109,10 @@ internal class DefaultNewsRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun fetchDetailedArticles(
|
||||
newsIds: Collection<Int>,
|
||||
language: String?,
|
||||
): Either<Map<Int, Throwable>, Unit> = fetchDetailedArticlesInternal(newsIds = newsIds, language = language)
|
||||
override suspend fun fetchDetailedArticles(newsIds: Collection<Int>): Either<Map<Int, Throwable>, Unit> =
|
||||
fetchDetailedArticlesInternal(newsIds = newsIds, language = language)
|
||||
|
||||
override suspend fun fetchTrendingNews(limit: Int, language: String?) {
|
||||
override suspend fun fetchTrendingNews(limit: Int) {
|
||||
fetchAndStoreTrendingNews(limit = limit, language = language)
|
||||
}
|
||||
|
||||
|
|
@ -259,7 +261,7 @@ internal class DefaultNewsRepository(
|
|||
)
|
||||
}
|
||||
|
||||
private class NewsBatchFetcher(
|
||||
private inner class NewsBatchFetcher(
|
||||
private val newsApi: NewsApi,
|
||||
private val batchSize: Int,
|
||||
private val newsViewedStore: NewsViewedStore,
|
||||
|
|
@ -326,7 +328,7 @@ internal class DefaultNewsRepository(
|
|||
val response = newsApi.getNews(
|
||||
page = page,
|
||||
limit = limit,
|
||||
language = params.language,
|
||||
language = language,
|
||||
snapshot = snapshotOverride?.takeIf { it.isNotEmpty() },
|
||||
tokenIds = params.tokenIds.takeIf { it.isNotEmpty() },
|
||||
categoryIds = params.categoryIds.takeIf { it.isNotEmpty() },
|
||||
|
|
|
|||
|
|
@ -568,7 +568,7 @@ internal class DefaultNFTRepository @Inject constructor(
|
|||
|
||||
private fun Network.canHandleNFTs(userWalletId: UserWalletId): Boolean {
|
||||
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
|
||||
val blockchain = Blockchain.fromNetworkId(backendId) ?: return false
|
||||
val blockchain = Blockchain.fromNetworkId(rawId) ?: return false
|
||||
|
||||
return blockchain.canHandleNFTs() &&
|
||||
userWallet.canHandleToken(blockchain, excludedBlockchains)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ dependencies {
|
|||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.common.ui)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
// endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.data.notifications.converters
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.core.ui.extensions.getActiveIconRes
|
||||
import com.tangem.common.ui.extensions.getActiveIconRes
|
||||
import com.tangem.datasource.api.tangemTech.models.CryptoNetworkResponse
|
||||
import com.tangem.domain.notifications.models.NotificationsEligibleNetwork
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ internal object NotificationsEligibleNetworkConverter {
|
|||
id = value.networkId,
|
||||
name = blockchain.fullName,
|
||||
symbol = blockchain.currency,
|
||||
icon = getActiveIconRes(blockchain.id),
|
||||
icon = getActiveIconRes(blockchain),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -249,7 +249,7 @@ internal class DefaultOnrampRepository(
|
|||
to = listOf(
|
||||
OnrampDestinationDTO(
|
||||
contractAddress = cryptoCurrency.getContractAddress(),
|
||||
network = cryptoCurrency.network.backendId,
|
||||
network = cryptoCurrency.network.rawId,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -314,7 +314,7 @@ internal class DefaultOnrampRepository(
|
|||
to = listOf(
|
||||
OnrampDestinationDTO(
|
||||
contractAddress = cryptoCurrency.getContractAddress(),
|
||||
network = cryptoCurrency.network.backendId,
|
||||
network = cryptoCurrency.network.rawId,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -363,7 +363,7 @@ internal class DefaultOnrampRepository(
|
|||
fromCurrencyCode = currency.code,
|
||||
fromPrecision = currency.precision,
|
||||
toContractAddress = cryptoCurrency.getContractAddress(),
|
||||
toNetwork = cryptoCurrency.network.backendId,
|
||||
toNetwork = cryptoCurrency.network.rawId,
|
||||
paymentMethod = paymentMethod.id,
|
||||
countryCode = country.code,
|
||||
fromAmount = fromAmount,
|
||||
|
|
@ -436,7 +436,7 @@ internal class DefaultOnrampRepository(
|
|||
fromCurrencyCode = currency.code,
|
||||
fromPrecision = currency.precision,
|
||||
toContractAddress = cryptoCurrency.getContractAddress(),
|
||||
toNetwork = cryptoCurrency.network.backendId,
|
||||
toNetwork = cryptoCurrency.network.rawId,
|
||||
paymentMethod = quote.paymentMethod.id,
|
||||
countryCode = country.code,
|
||||
fromAmount = fromAmountString,
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ internal class Bip321PaymentUriParserTest {
|
|||
|
||||
@Test
|
||||
fun `includes tokens on matching network`() {
|
||||
val btcToken = buildToken("BTC", "RUNE", "contractAddr")
|
||||
val btcToken = buildToken("bitcoin", "RUNE", "contractAddr")
|
||||
|
||||
val result = parser.parse(
|
||||
qrCode = "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=0.01",
|
||||
|
|
@ -263,7 +263,13 @@ internal class Bip321PaymentUriParserTest {
|
|||
|
||||
@Test
|
||||
fun `memo on network with memo support is not unsupported`() {
|
||||
val xrpCoin = buildCoin("XRP", "XRP", "XRP", decimals = 6, extrasType = Network.TransactionExtrasType.DESTINATION_TAG)
|
||||
val xrpCoin = buildCoin(
|
||||
rawNetworkId = "xrp",
|
||||
name = "XRP",
|
||||
symbol = "XRP",
|
||||
decimals = 6,
|
||||
extrasType = Network.TransactionExtrasType.DESTINATION_TAG,
|
||||
)
|
||||
|
||||
val result = parser.parse(
|
||||
qrCode = "ripple:rAddress?dt=12345",
|
||||
|
|
@ -299,9 +305,9 @@ internal class Bip321PaymentUriParserTest {
|
|||
}
|
||||
}
|
||||
|
||||
private val bitcoinCoin = buildCoin("BTC", "Bitcoin", "BTC", decimals = 8)
|
||||
private val litecoinCoin = buildCoin("LTC", "Litecoin", "LTC", decimals = 8)
|
||||
private val dogecoinCoin = buildCoin("DOGE", "Dogecoin", "DOGE", decimals = 8)
|
||||
private val bitcoinCoin = buildCoin("bitcoin", "Bitcoin", "BTC", decimals = 8)
|
||||
private val litecoinCoin = buildCoin("litecoin", "Litecoin", "LTC", decimals = 8)
|
||||
private val dogecoinCoin = buildCoin("dogecoin", "Dogecoin", "DOGE", decimals = 8)
|
||||
|
||||
private fun buildCoin(
|
||||
rawNetworkId: String,
|
||||
|
|
@ -349,8 +355,7 @@ internal class Bip321PaymentUriParserTest {
|
|||
extrasType: Network.TransactionExtrasType = Network.TransactionExtrasType.NONE,
|
||||
): Network {
|
||||
return Network(
|
||||
id = Network.ID(Network.RawID(rawNetworkId), Network.DerivationPath.None),
|
||||
backendId = rawNetworkId,
|
||||
id = Network.ID(value = rawNetworkId, derivationPath = Network.DerivationPath.None),
|
||||
name = name,
|
||||
currencySymbol = symbol,
|
||||
derivationPath = Network.DerivationPath.None,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.data.qrscanning
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.data.qrscanning.parser.QrContentClassifierParser
|
||||
import com.tangem.data.qrscanning.repository.DefaultQrScanningEventsRepository
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
@ -78,7 +77,8 @@ internal class DefaultQrScanningEventsRepositoryTest {
|
|||
|
||||
@Test
|
||||
fun testBip021() {
|
||||
every { network.id.rawId.value } returns Blockchain.Bitcoin.id
|
||||
every { network.id } returns Network.ID(value = "bitcoin", derivationPath = Network.DerivationPath.None)
|
||||
every { network.rawId } returns "bitcoin"
|
||||
positiveCase(
|
||||
"$schema1:$address1",
|
||||
QrResult(address = address1),
|
||||
|
|
@ -128,7 +128,8 @@ internal class DefaultQrScanningEventsRepositoryTest {
|
|||
|
||||
@Test
|
||||
fun testErc681Coin() {
|
||||
every { network.id.rawId.value } returns Blockchain.Ethereum.id
|
||||
every { network.id } returns Network.ID(value = "ethereum", derivationPath = Network.DerivationPath.None)
|
||||
every { network.rawId } returns "ethereum"
|
||||
positiveCase(
|
||||
address2,
|
||||
QrResult(address = address2),
|
||||
|
|
@ -183,7 +184,8 @@ internal class DefaultQrScanningEventsRepositoryTest {
|
|||
|
||||
@Test
|
||||
fun testErc681Token() {
|
||||
every { network.id.rawId.value } returns Blockchain.Ethereum.id
|
||||
every { network.id } returns Network.ID(value = "ethereum", derivationPath = Network.DerivationPath.None)
|
||||
every { network.rawId } returns "ethereum"
|
||||
positiveCase(
|
||||
address2,
|
||||
QrResult(address = address2),
|
||||
|
|
|
|||
|
|
@ -382,8 +382,7 @@ internal class Eip681PaymentUriParserTest {
|
|||
|
||||
private fun buildNetwork(rawNetworkId: String): Network {
|
||||
return Network(
|
||||
id = Network.ID(Network.RawID(rawNetworkId), Network.DerivationPath.None),
|
||||
backendId = rawNetworkId,
|
||||
id = Network.ID(value = rawNetworkId, derivationPath = Network.DerivationPath.None),
|
||||
name = rawNetworkId,
|
||||
currencySymbol = rawNetworkId.take(3).uppercase(),
|
||||
derivationPath = Network.DerivationPath.None,
|
||||
|
|
|
|||
|
|
@ -233,8 +233,7 @@ internal class QrContentClassifierTest {
|
|||
|
||||
private fun buildNetwork(rawNetworkId: String): Network {
|
||||
return Network(
|
||||
id = Network.ID(Network.RawID(rawNetworkId), Network.DerivationPath.None),
|
||||
backendId = rawNetworkId,
|
||||
id = Network.ID(value = rawNetworkId, derivationPath = Network.DerivationPath.None),
|
||||
name = rawNetworkId,
|
||||
currencySymbol = rawNetworkId.take(3).uppercase(),
|
||||
derivationPath = Network.DerivationPath.None,
|
||||
|
|
|
|||
|
|
@ -198,13 +198,13 @@ internal class SolanaPaymentUriParserTest {
|
|||
}
|
||||
}
|
||||
|
||||
private val solanaNetwork = buildNetwork("SOLANA", "Solana", "SOL")
|
||||
private val solanaNetwork = buildNetwork("solana", "Solana", "SOL")
|
||||
|
||||
private val solanaCoin = CryptoCurrency.Coin(
|
||||
id = CryptoCurrency.ID(
|
||||
prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX,
|
||||
body = CryptoCurrency.ID.Body.NetworkId("SOLANA"),
|
||||
suffix = CryptoCurrency.ID.Suffix.RawID("SOLANA"),
|
||||
body = CryptoCurrency.ID.Body.NetworkId("solana"),
|
||||
suffix = CryptoCurrency.ID.Suffix.RawID("solana"),
|
||||
),
|
||||
network = solanaNetwork,
|
||||
name = "Solana",
|
||||
|
|
@ -217,7 +217,7 @@ internal class SolanaPaymentUriParserTest {
|
|||
private val usdcToken = CryptoCurrency.Token(
|
||||
id = CryptoCurrency.ID(
|
||||
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
|
||||
body = CryptoCurrency.ID.Body.NetworkId("SOLANA"),
|
||||
body = CryptoCurrency.ID.Body.NetworkId("solana"),
|
||||
suffix = CryptoCurrency.ID.Suffix.RawID("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"),
|
||||
),
|
||||
network = solanaNetwork,
|
||||
|
|
@ -231,8 +231,7 @@ internal class SolanaPaymentUriParserTest {
|
|||
|
||||
private fun buildNetwork(rawNetworkId: String, name: String, symbol: String): Network {
|
||||
return Network(
|
||||
id = Network.ID(Network.RawID(rawNetworkId), Network.DerivationPath.None),
|
||||
backendId = rawNetworkId,
|
||||
id = Network.ID(value = rawNetworkId, derivationPath = Network.DerivationPath.None),
|
||||
name = name,
|
||||
currencySymbol = symbol,
|
||||
derivationPath = Network.DerivationPath.None,
|
||||
|
|
|
|||
|
|
@ -238,13 +238,13 @@ internal class TronPaymentUriParserTest {
|
|||
}
|
||||
}
|
||||
|
||||
private val tronNetwork = buildNetwork("TRON", "Tron", "TRX")
|
||||
private val tronNetwork = buildNetwork("tron", "Tron", "TRX")
|
||||
|
||||
private val tronCoin = CryptoCurrency.Coin(
|
||||
id = CryptoCurrency.ID(
|
||||
prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX,
|
||||
body = CryptoCurrency.ID.Body.NetworkId("TRON"),
|
||||
suffix = CryptoCurrency.ID.Suffix.RawID("TRON"),
|
||||
body = CryptoCurrency.ID.Body.NetworkId("tron"),
|
||||
suffix = CryptoCurrency.ID.Suffix.RawID("tron"),
|
||||
),
|
||||
network = tronNetwork,
|
||||
name = "Tron",
|
||||
|
|
@ -257,7 +257,7 @@ internal class TronPaymentUriParserTest {
|
|||
private val usdtToken = CryptoCurrency.Token(
|
||||
id = CryptoCurrency.ID(
|
||||
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
|
||||
body = CryptoCurrency.ID.Body.NetworkId("TRON"),
|
||||
body = CryptoCurrency.ID.Body.NetworkId("tron"),
|
||||
suffix = CryptoCurrency.ID.Suffix.RawID("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"),
|
||||
),
|
||||
network = tronNetwork,
|
||||
|
|
@ -271,8 +271,7 @@ internal class TronPaymentUriParserTest {
|
|||
|
||||
private fun buildNetwork(rawNetworkId: String, name: String, symbol: String): Network {
|
||||
return Network(
|
||||
id = Network.ID(Network.RawID(rawNetworkId), Network.DerivationPath.None),
|
||||
backendId = rawNetworkId,
|
||||
id = Network.ID(value = rawNetworkId, derivationPath = Network.DerivationPath.None),
|
||||
name = name,
|
||||
currencySymbol = symbol,
|
||||
derivationPath = Network.DerivationPath.None,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ android {
|
|||
namespace = "com.tangem.data.settings"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(projects.core.datasource)
|
||||
|
|
@ -28,6 +32,11 @@ dependencies {
|
|||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
// region Test
|
||||
testImplementation(projects.test.core)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
// endregion
|
||||
|
||||
// region Others dependencies
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
package com.tangem.data.settings
|
||||
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.utils.get
|
||||
import com.tangem.domain.settings.HotWalletRestrictionManager
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
/**
|
||||
* Development implementation of [HotWalletRestrictionManager].
|
||||
*
|
||||
* Reads and writes the restriction state from [AppPreferencesStore],
|
||||
* allowing testers to toggle it via the Tester Menu.
|
||||
* The preference [Flow] is converted to a [StateFlow] on construction,
|
||||
* so [isCreationEnabledSync] can be called from non-suspending contexts.
|
||||
* Defaults to `true` (restriction enabled) when no value is stored.
|
||||
*/
|
||||
internal class DevHotWalletRestrictionManager(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
) : HotWalletRestrictionManager {
|
||||
|
||||
private val isCreationEnabledState: StateFlow<Boolean> =
|
||||
appPreferencesStore
|
||||
.get(key = IS_HOT_WALLET_CREATION_RESTRICTION_ENABLED_KEY, default = true)
|
||||
.stateIn(
|
||||
scope = CoroutineScope(dispatchers.io + SupervisorJob()),
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = true,
|
||||
)
|
||||
|
||||
override fun isCreationEnabled(): StateFlow<Boolean> = isCreationEnabledState
|
||||
|
||||
override fun isCreationEnabledSync(): Boolean = isCreationEnabledState.value
|
||||
|
||||
override suspend fun toggleCreationEnabled() {
|
||||
appPreferencesStore.editData { preferences ->
|
||||
val isEnabled = preferences.getOrDefault(
|
||||
key = IS_HOT_WALLET_CREATION_RESTRICTION_ENABLED_KEY,
|
||||
default = true,
|
||||
)
|
||||
preferences[IS_HOT_WALLET_CREATION_RESTRICTION_ENABLED_KEY] = !isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val IS_HOT_WALLET_CREATION_RESTRICTION_ENABLED_KEY =
|
||||
booleanPreferencesKey(name = "isHotWalletCreationRestrictionEnabled")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.data.settings
|
||||
|
||||
import com.tangem.domain.settings.HotWalletRestrictionManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Production implementation of [HotWalletRestrictionManager].
|
||||
*
|
||||
* Hot wallet creation restriction is always enabled in production builds —
|
||||
* users must scan a physical Tangem card to add a wallet.
|
||||
* [toggleCreationEnabled] is a no-op since the restriction cannot be changed at runtime.
|
||||
*/
|
||||
internal class ProdHotWalletRestrictionManager : HotWalletRestrictionManager {
|
||||
|
||||
private val state: StateFlow<Boolean> = MutableStateFlow(true)
|
||||
|
||||
override fun isCreationEnabled(): StateFlow<Boolean> = state
|
||||
override fun isCreationEnabledSync(): Boolean = true
|
||||
override suspend fun toggleCreationEnabled() = Unit
|
||||
}
|
||||
|
|
@ -1,12 +1,16 @@
|
|||
package com.tangem.data.settings.di
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.data.settings.BuildConfig
|
||||
import com.tangem.data.settings.DefaultAppRatingRepository
|
||||
import com.tangem.data.settings.DefaultPermissionRepository
|
||||
import com.tangem.data.settings.DefaultSettingsRepository
|
||||
import com.tangem.data.settings.DevHotWalletRestrictionManager
|
||||
import com.tangem.data.settings.ProdHotWalletRestrictionManager
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.logs.AppLogsStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.settings.HotWalletRestrictionManager
|
||||
import com.tangem.domain.settings.repositories.AppRatingRepository
|
||||
import com.tangem.domain.settings.repositories.PermissionRepository
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
|
|
@ -55,4 +59,17 @@ internal object SettingsDataModule {
|
|||
context = context,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideHotWalletRestrictionManager(
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): HotWalletRestrictionManager {
|
||||
return if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
DevHotWalletRestrictionManager(appPreferencesStore, dispatchers)
|
||||
} else {
|
||||
ProdHotWalletRestrictionManager()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package com.tangem.data.settings
|
||||
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.utils.get
|
||||
import com.tangem.test.core.getEmittedValues
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DevHotWalletRestrictionManagerTest {
|
||||
|
||||
private val appPreferencesStore = mockk<AppPreferencesStore>(relaxed = true)
|
||||
private val dispatchers: CoroutineDispatcherProvider = TestingCoroutineDispatcherProvider()
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic("com.tangem.datasource.local.preferences.utils.PreferencesDataStoreExtKt")
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic("com.tangem.datasource.local.preferences.utils.PreferencesDataStoreExtKt")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN preference is true WHEN isCreationEnabled THEN emits true`() = runTest {
|
||||
every {
|
||||
appPreferencesStore.get(key = any<Preferences.Key<Boolean>>(), default = true)
|
||||
} returns flowOf(true)
|
||||
|
||||
val manager = DevHotWalletRestrictionManager(appPreferencesStore, dispatchers)
|
||||
|
||||
val emitted = getEmittedValues(manager.isCreationEnabled())
|
||||
|
||||
assertThat(emitted).containsExactly(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN preference is false WHEN isCreationEnabled THEN emits false`() = runTest {
|
||||
every {
|
||||
appPreferencesStore.get(key = any<Preferences.Key<Boolean>>(), default = true)
|
||||
} returns flowOf(false)
|
||||
|
||||
val manager = DevHotWalletRestrictionManager(appPreferencesStore, dispatchers)
|
||||
|
||||
val emitted = getEmittedValues(manager.isCreationEnabled())
|
||||
|
||||
assertThat(emitted).containsExactly(false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN preference Flow emits true WHEN isCreationEnabledSync THEN returns cached true`() = runTest {
|
||||
every {
|
||||
appPreferencesStore.get(key = any<Preferences.Key<Boolean>>(), default = true)
|
||||
} returns flowOf(true)
|
||||
|
||||
val manager = DevHotWalletRestrictionManager(appPreferencesStore, dispatchers)
|
||||
|
||||
assertThat(manager.isCreationEnabledSync()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN preference Flow emits false WHEN isCreationEnabledSync THEN returns cached false`() = runTest {
|
||||
every {
|
||||
appPreferencesStore.get(key = any<Preferences.Key<Boolean>>(), default = true)
|
||||
} returns flowOf(false)
|
||||
|
||||
val manager = DevHotWalletRestrictionManager(appPreferencesStore, dispatchers)
|
||||
|
||||
assertThat(manager.isCreationEnabledSync()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN toggleCreationEnabled THEN opens editData transaction`() = runTest {
|
||||
every {
|
||||
appPreferencesStore.get(key = any<Preferences.Key<Boolean>>(), default = true)
|
||||
} returns flowOf(true)
|
||||
coEvery { appPreferencesStore.editData(any()) } returns mockk(relaxed = true)
|
||||
|
||||
val manager = DevHotWalletRestrictionManager(appPreferencesStore, dispatchers)
|
||||
manager.toggleCreationEnabled()
|
||||
|
||||
coVerify { appPreferencesStore.editData(any()) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.data.settings
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.test.core.getEmittedValues
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class ProdHotWalletRestrictionManagerTest {
|
||||
|
||||
private val manager = ProdHotWalletRestrictionManager()
|
||||
|
||||
@Test
|
||||
fun `WHEN isCreationEnabled THEN always emits true`() = runTest {
|
||||
val emitted = getEmittedValues(manager.isCreationEnabled())
|
||||
|
||||
assertThat(emitted).containsExactly(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN isCreationEnabledSync THEN returns true`() = runTest {
|
||||
assertThat(manager.isCreationEnabledSync()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN toggleCreationEnabled THEN isCreationEnabledSync still returns true`() = runTest {
|
||||
manager.toggleCreationEnabled()
|
||||
assertThat(manager.isCreationEnabledSync()).isTrue()
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolUnsignedTx
|
|||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.staking.toggles.StakingFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
|
@ -65,7 +66,7 @@ internal class DefaultP2PEthPoolRepository(
|
|||
}
|
||||
|
||||
override suspend fun fetchVaults(network: P2PEthPoolNetwork) {
|
||||
val vaults = if (stakingFeatureToggles.isEthStakingEnabled) {
|
||||
val vaults = if (stakingFeatureToggles.isIntegrationEnabled(StakingIntegrationID.P2PEthPool)) {
|
||||
getVaults(network).getOrElse { error ->
|
||||
TangemLogger.e("Error fetching P2PEthPool vaults: $error")
|
||||
emptyList()
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.blockchain.common.TransactionData
|
|||
import com.tangem.blockchain.common.TransactionStatus
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toCompressedPublicKey
|
||||
import com.tangem.data.staking.converters.YieldConverter
|
||||
|
|
@ -244,7 +245,7 @@ internal class DefaultStakeKitRepository(
|
|||
}
|
||||
|
||||
override suspend fun constructTransaction(
|
||||
networkId: String,
|
||||
networkId: Network.RawID,
|
||||
fee: Fee,
|
||||
amount: Amount,
|
||||
transactionId: String,
|
||||
|
|
@ -320,8 +321,11 @@ internal class DefaultStakeKitRepository(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getTransactionDataType(networkId: String, unsignedTransaction: String): TransactionData.Compiled.Data {
|
||||
return when (Blockchain.fromId(networkId)) {
|
||||
private fun getTransactionDataType(
|
||||
networkId: Network.RawID,
|
||||
unsignedTransaction: String,
|
||||
): TransactionData.Compiled.Data {
|
||||
return when (val blockchain = networkId.toBlockchain()) {
|
||||
Blockchain.Solana,
|
||||
Blockchain.Cosmos,
|
||||
-> TransactionData.Compiled.Data.Bytes(unsignedTransaction.hexToBytes())
|
||||
|
|
@ -335,7 +339,7 @@ internal class DefaultStakeKitRepository(
|
|||
?: error("Failed to parse Tron StakeKit transaction")
|
||||
TransactionData.Compiled.Data.RawString(tronStakeKitTransaction.rawDataHex)
|
||||
}
|
||||
else -> error("Unsupported blockchain")
|
||||
else -> error("Unsupported blockchain: $blockchain")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -411,7 +415,7 @@ internal class DefaultStakeKitRepository(
|
|||
}
|
||||
|
||||
private fun getTronResource(network: Network): TronResource? {
|
||||
val blockchain = Blockchain.fromNetworkId(network.backendId)
|
||||
val blockchain = Blockchain.fromNetworkId(network.rawId)
|
||||
|
||||
return if (blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet) {
|
||||
TronResource.ENERGY
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package com.tangem.data.staking
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.data.staking.store.StakeKitBalancesStore
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isWallet2
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
@ -22,14 +20,13 @@ import com.tangem.lib.crypto.BlockchainUtils.isSolana
|
|||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
|
||||
internal class DefaultStakingRepository(
|
||||
private val stakeKitRepository: StakeKitRepository,
|
||||
private val p2pEthPoolRepository: P2PEthPoolRepository,
|
||||
private val stakingBalanceStoreV2: StakeKitBalancesStore,
|
||||
private val stakeKitBalancesStore: StakeKitBalancesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val stakingFeatureToggles: StakingFeatureToggles,
|
||||
|
|
@ -40,7 +37,8 @@ internal class DefaultStakingRepository(
|
|||
cryptoCurrency: CryptoCurrency,
|
||||
): Flow<StakingAvailability> {
|
||||
return channelFlow {
|
||||
if (!checkFeatureToggleEnabled(cryptoCurrency)) {
|
||||
val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
|
||||
if (stakingIntegration == null || !stakingFeatureToggles.isIntegrationEnabled(stakingIntegration)) {
|
||||
send(StakingAvailability.Unavailable)
|
||||
return@channelFlow
|
||||
}
|
||||
|
|
@ -56,8 +54,6 @@ internal class DefaultStakingRepository(
|
|||
return@channelFlow
|
||||
}
|
||||
|
||||
val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
|
||||
|
||||
val availabilityFlow = when (stakingIntegration) {
|
||||
StakingIntegrationID.P2PEthPool -> p2pEthPoolRepository.getStakingAvailability()
|
||||
is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailability(
|
||||
|
|
@ -65,7 +61,6 @@ internal class DefaultStakingRepository(
|
|||
rawCurrencyId,
|
||||
cryptoCurrency.symbol,
|
||||
)
|
||||
null -> flowOf(StakingAvailability.Unavailable)
|
||||
}
|
||||
|
||||
availabilityFlow.collect { send(it) }
|
||||
|
|
@ -76,20 +71,15 @@ internal class DefaultStakingRepository(
|
|||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): StakingAvailability {
|
||||
if (!checkFeatureToggleEnabled(cryptoCurrency)) {
|
||||
return StakingAvailability.Unavailable
|
||||
}
|
||||
val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
|
||||
?.takeIf(stakingFeatureToggles::isIntegrationEnabled)
|
||||
?: return StakingAvailability.Unavailable
|
||||
|
||||
if (checkForInvalidCardBatch(userWalletId, cryptoCurrency)) {
|
||||
return StakingAvailability.Unavailable
|
||||
}
|
||||
|
||||
val rawCurrencyId = cryptoCurrency.id.rawCurrencyId
|
||||
if (rawCurrencyId == null) {
|
||||
return StakingAvailability.Unavailable
|
||||
}
|
||||
|
||||
val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
|
||||
?: return StakingAvailability.Unavailable
|
||||
|
||||
return when (stakingIntegration) {
|
||||
|
|
@ -104,7 +94,7 @@ internal class DefaultStakingRepository(
|
|||
|
||||
override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean {
|
||||
return withContext(dispatchers.default) {
|
||||
val balances = stakingBalanceStoreV2.getAllSyncOrNull(userWalletId) ?: return@withContext false
|
||||
val balances = stakeKitBalancesStore.getAllSyncOrNull(userWalletId) ?: return@withContext false
|
||||
|
||||
val hasDataStakingBalance by lazy {
|
||||
balances.any { stakingBalance ->
|
||||
|
|
@ -116,18 +106,6 @@ internal class DefaultStakingRepository(
|
|||
}
|
||||
}
|
||||
|
||||
private fun checkFeatureToggleEnabled(cryptoCurrency: CryptoCurrency): Boolean {
|
||||
return when (cryptoCurrency.network.id.toBlockchain()) {
|
||||
Blockchain.Ethereum -> {
|
||||
when (cryptoCurrency) {
|
||||
is CryptoCurrency.Coin -> stakingFeatureToggles.isEthStakingEnabled
|
||||
is CryptoCurrency.Token -> true
|
||||
}
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkForInvalidCardBatch(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
|
||||
val userWallet = getUserWalletUseCase(userWalletId).getOrElse {
|
||||
error("Failed to get user wallet")
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ import com.tangem.data.staking.store.StakeKitBalancesStore
|
|||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceProducer
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier
|
||||
import com.tangem.domain.staking.single.SingleStakingBalanceProducer
|
||||
import com.tangem.domain.staking.single.SingleStakingBalanceSupplier
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -54,7 +54,7 @@ internal object StakingBalanceSupplierModule {
|
|||
fun provideSingleStakingBalanceSupplier(
|
||||
factory: SingleStakingBalanceProducer.Factory,
|
||||
): SingleStakingBalanceSupplier {
|
||||
return object : SingleStakingBalanceSupplier(
|
||||
return SingleStakingBalanceSupplier(
|
||||
factory = factory,
|
||||
keyCreator = { params ->
|
||||
listOf(
|
||||
|
|
@ -65,15 +65,15 @@ internal object StakingBalanceSupplierModule {
|
|||
)
|
||||
.joinToString(separator = "_")
|
||||
},
|
||||
) {}
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMultiStakingBalanceSupplier(factory: MultiStakingBalanceProducer.Factory): MultiStakingBalanceSupplier {
|
||||
return object : MultiStakingBalanceSupplier(
|
||||
return MultiStakingBalanceSupplier(
|
||||
factory = factory,
|
||||
keyCreator = { "multi_staking_balances_${it.userWalletId.stringValue}" },
|
||||
) {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ internal object StakingDataModule {
|
|||
return DefaultStakingRepository(
|
||||
stakeKitRepository = stakeKitRepository,
|
||||
p2pEthPoolRepository = p2pEthPoolRepository,
|
||||
stakingBalanceStoreV2 = stakeKitBalancesStore,
|
||||
stakeKitBalancesStore = stakeKitBalancesStore,
|
||||
dispatchers = dispatchers,
|
||||
getUserWalletUseCase = getUserWalletUseCase,
|
||||
stakingFeatureToggles = stakingFeatureToggles,
|
||||
|
|
|
|||
|
|
@ -2,12 +2,35 @@ package com.tangem.data.staking.toggles
|
|||
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.staking.toggles.StakingFeatureToggles
|
||||
|
||||
internal class DefaultStakingFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : StakingFeatureToggles {
|
||||
|
||||
override val isEthStakingEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED)
|
||||
override fun isIntegrationEnabled(integrationId: StakingIntegrationID): Boolean {
|
||||
val toggle = integrationId.getFeatureToggle() ?: return true
|
||||
return featureTogglesManager.isFeatureEnabled(toggle)
|
||||
}
|
||||
|
||||
private fun StakingIntegrationID.getFeatureToggle(): FeatureToggles? = when (this) {
|
||||
is StakingIntegrationID.P2PEthPool -> FeatureToggles.STAKING_ETH_ENABLED
|
||||
is StakingIntegrationID.StakeKit -> this.getStakeKitFeatureToggle()
|
||||
}
|
||||
|
||||
private fun StakingIntegrationID.StakeKit.getStakeKitFeatureToggle(): FeatureToggles? = when (this) {
|
||||
is StakingIntegrationID.StakeKit.Coin -> when (this) {
|
||||
StakingIntegrationID.StakeKit.Coin.Ton,
|
||||
StakingIntegrationID.StakeKit.Coin.Solana,
|
||||
StakingIntegrationID.StakeKit.Coin.Cosmos,
|
||||
StakingIntegrationID.StakeKit.Coin.Tron,
|
||||
StakingIntegrationID.StakeKit.Coin.BSC,
|
||||
StakingIntegrationID.StakeKit.Coin.Cardano,
|
||||
-> null
|
||||
}
|
||||
is StakingIntegrationID.StakeKit.EthereumToken -> when (this) {
|
||||
StakingIntegrationID.StakeKit.EthereumToken.Polygon -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.tangem.data.staking.toggles
|
||||
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultStakingFeatureTogglesTest {
|
||||
|
||||
private val featureTogglesManager: FeatureTogglesManager = mockk()
|
||||
private val toggles = DefaultStakingFeatureToggles(featureTogglesManager = featureTogglesManager)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(featureTogglesManager)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `P2PEthPool returns true when STAKING_ETH_ENABLED is enabled`() {
|
||||
every { featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED) } returns true
|
||||
|
||||
assertThat(toggles.isIntegrationEnabled(StakingIntegrationID.P2PEthPool)).isTrue()
|
||||
|
||||
verify(exactly = 1) { featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `P2PEthPool returns false when STAKING_ETH_ENABLED is disabled`() {
|
||||
every { featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED) } returns false
|
||||
|
||||
assertThat(toggles.isIntegrationEnabled(StakingIntegrationID.P2PEthPool)).isFalse()
|
||||
|
||||
verify(exactly = 1) { featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `existing StakeKit Coin integrations are always enabled`() {
|
||||
StakingIntegrationID.StakeKit.Coin.entries.forEach { coin ->
|
||||
assertThat(toggles.isIntegrationEnabled(coin)).isTrue()
|
||||
}
|
||||
|
||||
verify(exactly = 0) { featureTogglesManager.isFeatureEnabled(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `existing StakeKit EthereumToken integrations are always enabled`() {
|
||||
StakingIntegrationID.StakeKit.EthereumToken.entries.forEach { token ->
|
||||
assertThat(toggles.isIntegrationEnabled(token)).isTrue()
|
||||
}
|
||||
|
||||
verify(exactly = 0) { featureTogglesManager.isFeatureEnabled(any()) }
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,10 @@ android {
|
|||
namespace = "com.tangem.data.swap"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Core */
|
||||
implementation(projects.core.datasource)
|
||||
|
|
@ -56,4 +60,10 @@ dependencies {
|
|||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Test */
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(tangemDeps.card.core)
|
||||
testImplementation(projects.common.test)
|
||||
testImplementation(projects.test.core)
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import com.tangem.data.swap.converter.TokenInfoConverter
|
|||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.express.models.request.ExchangeSentRequestBody
|
||||
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
|
||||
import com.tangem.datasource.api.express.models.request.PairsRequestBody
|
||||
import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWithTxDetails
|
||||
import com.tangem.datasource.api.express.models.response.RateType
|
||||
|
|
@ -34,7 +35,6 @@ import com.tangem.domain.quotes.single.SingleQuoteStatusProducer
|
|||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.swap.SwapRepositoryV2
|
||||
import com.tangem.domain.swap.models.*
|
||||
import com.tangem.domain.swap.models.SwapAmountType
|
||||
import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
|
@ -44,6 +44,7 @@ import kotlinx.coroutines.awaitAll
|
|||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -64,6 +65,76 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
private val exchangeStatusConverter = SwapStatusConverter()
|
||||
private val txDetailsMoshiAdapter = moshi.adapter(TxDetails::class.java)
|
||||
|
||||
override suspend fun getPairs(
|
||||
primarySwapCurrencyStatus: SwapCurrencyStatus,
|
||||
secondarySwapCurrencyStatus: SwapCurrencyStatus,
|
||||
filterProviderTypes: List<ExpressProviderType>,
|
||||
swapTxType: SwapTxType,
|
||||
): List<SwapPairModel> = withContext(coroutineDispatcher.default) {
|
||||
val primaryCurrency = primarySwapCurrencyStatus.currency
|
||||
val secondaryCurrency = secondarySwapCurrencyStatus.currency
|
||||
val primaryUserWallet = primarySwapCurrencyStatus.userWallet
|
||||
val secondaryUserWallet = secondarySwapCurrencyStatus.userWallet
|
||||
|
||||
val pairs = awaitAll(
|
||||
// original pairs
|
||||
async {
|
||||
invokePairRequest(
|
||||
userWallet = primaryUserWallet,
|
||||
from = listOf(primaryCurrency),
|
||||
to = listOf(secondaryCurrency),
|
||||
)
|
||||
},
|
||||
// reversed pairs
|
||||
async {
|
||||
invokePairRequest(
|
||||
userWallet = secondaryUserWallet,
|
||||
from = listOf(secondaryCurrency),
|
||||
to = listOf(primaryCurrency),
|
||||
)
|
||||
},
|
||||
).flatten()
|
||||
|
||||
val expressProviders = expressRepository.getFilteredProviders(
|
||||
userWallet = primaryUserWallet,
|
||||
filterProviderTypes = filterProviderTypes,
|
||||
swapTxType = swapTxType,
|
||||
).associateBy(ExpressProvider::providerId)
|
||||
|
||||
fun checkPair(currency: CryptoCurrency, pair: LeastTokenInfo): Boolean {
|
||||
return currency.getContractAddress() == pair.contractAddress &&
|
||||
currency.network.rawId == pair.network
|
||||
}
|
||||
|
||||
pairs.mapNotNull { pair ->
|
||||
val fromCurrencyStatus = when {
|
||||
checkPair(primaryCurrency, pair.from) -> primarySwapCurrencyStatus.status
|
||||
checkPair(secondaryCurrency, pair.from) -> secondarySwapCurrencyStatus.status
|
||||
else -> null
|
||||
}
|
||||
|
||||
val toCurrencyStatus = when {
|
||||
checkPair(primaryCurrency, pair.to) -> primarySwapCurrencyStatus.status
|
||||
checkPair(secondaryCurrency, pair.to) -> secondarySwapCurrencyStatus.status
|
||||
else -> null
|
||||
}
|
||||
|
||||
val mappedProviders = pair.providers
|
||||
.mapNotNull { it.withExpressProvider(expressProviders) }
|
||||
.filterYieldSupplyProvider(fromCurrencyStatus)
|
||||
|
||||
if (fromCurrencyStatus != null && toCurrencyStatus != null && mappedProviders.isNotEmpty()) {
|
||||
SwapPairModel(
|
||||
from = fromCurrencyStatus,
|
||||
to = toCurrencyStatus,
|
||||
providers = mappedProviders,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getPairs(
|
||||
userWallet: UserWallet,
|
||||
initialCurrency: CryptoCurrency,
|
||||
|
|
@ -91,12 +162,12 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
val statusFrom = cryptoCurrencyStatusList
|
||||
.firstOrNull { currencyStatus ->
|
||||
currencyStatus.currency.getContractAddress() == pair.from.contractAddress &&
|
||||
currencyStatus.currency.network.backendId == pair.from.network
|
||||
currencyStatus.currency.network.rawId == pair.from.network
|
||||
}
|
||||
val statusTo = cryptoCurrencyStatusList
|
||||
.firstOrNull { currencyStatus ->
|
||||
currencyStatus.currency.getContractAddress() == pair.to.contractAddress &&
|
||||
currencyStatus.currency.network.backendId == pair.to.network
|
||||
currencyStatus.currency.network.rawId == pair.to.network
|
||||
}
|
||||
|
||||
val mappedProviders = pair.providers
|
||||
|
|
@ -143,14 +214,14 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
cryptoCurrencyList
|
||||
.firstOrNull { currency ->
|
||||
currency.getContractAddress() == pair.from.contractAddress &&
|
||||
currency.network.backendId == pair.from.network
|
||||
currency.network.rawId == pair.from.network
|
||||
}
|
||||
}
|
||||
val statusToDeferred = async {
|
||||
cryptoCurrencyList
|
||||
.firstOrNull { currency ->
|
||||
currency.getContractAddress() == pair.to.contractAddress &&
|
||||
currency.network.backendId == pair.to.network
|
||||
currency.network.rawId == pair.to.network
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -185,23 +256,19 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
): SwapQuoteModel = withContext(coroutineDispatcher.io) {
|
||||
val response = tangemExpressApi.getExchangeQuote(
|
||||
fromAmount = if (amountType == SwapAmountType.From) {
|
||||
amount.movePointRight(
|
||||
fromCryptoCurrency.decimals,
|
||||
).toString()
|
||||
amount.toStringWithRightOffset(fromCryptoCurrency.decimals)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
toAmount = if (amountType == SwapAmountType.To) {
|
||||
amount.movePointRight(
|
||||
toCryptoCurrency.decimals,
|
||||
).toString()
|
||||
amount.toStringWithRightOffset(toCryptoCurrency.decimals)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
fromNetwork = fromCryptoCurrency.network.backendId,
|
||||
fromNetwork = fromCryptoCurrency.network.rawId,
|
||||
fromContractAddress = fromCryptoCurrency.getContractAddress(),
|
||||
fromDecimals = fromCryptoCurrency.decimals,
|
||||
toNetwork = toCryptoCurrency.network.backendId,
|
||||
toNetwork = toCryptoCurrency.network.rawId,
|
||||
toContractAddress = toCryptoCurrency.getContractAddress(),
|
||||
toDecimals = toCryptoCurrency.decimals,
|
||||
providerId = provider.providerId,
|
||||
|
|
@ -229,7 +296,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
userWallet: UserWallet,
|
||||
fromCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
toCryptoCurrency: CryptoCurrency,
|
||||
amount: String,
|
||||
amount: BigDecimal,
|
||||
amountType: SwapAmountType,
|
||||
toAddress: String,
|
||||
toExtraId: String?,
|
||||
|
|
@ -255,14 +322,22 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
val response = tangemExpressApi.getExchangeData(
|
||||
fromContractAddress = fromCurrency.getContractAddress(),
|
||||
toContractAddress = toCryptoCurrency.getContractAddress(),
|
||||
fromNetwork = fromCurrency.network.backendId,
|
||||
toNetwork = toCryptoCurrency.network.backendId,
|
||||
fromNetwork = fromCurrency.network.rawId,
|
||||
toNetwork = toCryptoCurrency.network.rawId,
|
||||
fromAddress = fromStatus.networkAddress?.defaultAddress?.value.orEmpty(),
|
||||
toAddress = toAddress,
|
||||
fromDecimals = fromCurrency.decimals,
|
||||
toDecimals = toCryptoCurrency.decimals,
|
||||
fromAmount = if (amountType == SwapAmountType.From) amount else null,
|
||||
toAmount = if (amountType == SwapAmountType.To) amount else null,
|
||||
fromAmount = if (amountType == SwapAmountType.From) {
|
||||
amount.toStringWithRightOffset(fromCurrency.decimals)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
toAmount = if (amountType == SwapAmountType.To) {
|
||||
amount.toStringWithRightOffset(toCryptoCurrency.decimals)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
providerId = expressProvider.providerId,
|
||||
rateType = rateType.name.lowercase(),
|
||||
requestId = requestId,
|
||||
|
|
@ -316,7 +391,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
),
|
||||
body = ExchangeSentRequestBody(
|
||||
txId = txId,
|
||||
fromNetwork = currency.network.backendId,
|
||||
fromNetwork = currency.network.rawId,
|
||||
fromAddress = status.networkAddress?.defaultAddress?.value.orEmpty(),
|
||||
payinAddress = payInAddress,
|
||||
payinExtraId = txExtraId,
|
||||
|
|
@ -465,6 +540,10 @@ internal class DefaultSwapRepositoryV2 @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun BigDecimal.toStringWithRightOffset(decimals: Int): String {
|
||||
return setScale(decimals, RoundingMode.HALF_DOWN).movePointRight(decimals).toPlainString()
|
||||
}
|
||||
|
||||
private fun List<ExpressProvider>.filterYieldSupplyProvider(cryptoCurrencyStatus: CryptoCurrencyStatus?) =
|
||||
filter { provider ->
|
||||
// !!!WARNING!!! Filter out dex provider if yield supply is active
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@ internal class DefaultSwapTransactionRepository(
|
|||
private val userTokensResponseFactory = UserTokensResponseFactory()
|
||||
|
||||
override suspend fun storeTransaction(
|
||||
userWalletId: UserWalletId,
|
||||
fromUserWalletId: UserWalletId,
|
||||
toUserWalletId: UserWalletId,
|
||||
fromCryptoCurrency: CryptoCurrency,
|
||||
toCryptoCurrency: CryptoCurrency,
|
||||
fromAccount: Account?,
|
||||
|
|
@ -75,7 +76,10 @@ internal class DefaultSwapTransactionRepository(
|
|||
val tokenTransactions = savedTransactions
|
||||
?.firstOrNull { swapTxList ->
|
||||
swapTxList.checkId(
|
||||
checkUserWalletId = userWalletId,
|
||||
fromUserWalletId = fromUserWalletId,
|
||||
toUserWalletId = toUserWalletId,
|
||||
fromAccountId = fromAccount?.accountId,
|
||||
toAccountId = toAccount?.accountId,
|
||||
fromCurrencyId = fromCryptoCurrency.id,
|
||||
toCurrencyId = toCryptoCurrency.id,
|
||||
)
|
||||
|
|
@ -89,7 +93,8 @@ internal class DefaultSwapTransactionRepository(
|
|||
mutablePreferences.setObject(
|
||||
key = PreferencesKeys.SWAP_TRANSACTIONS_KEY,
|
||||
value = savedTransactions?.updateList(
|
||||
userWalletId = userWalletId,
|
||||
fromUserWalletId = fromUserWalletId,
|
||||
toUserWalletId = toUserWalletId,
|
||||
fromCryptoCurrency = fromCryptoCurrency,
|
||||
toCryptoCurrency = toCryptoCurrency,
|
||||
fromAccount = fromAccount,
|
||||
|
|
@ -97,7 +102,8 @@ internal class DefaultSwapTransactionRepository(
|
|||
transactions = tokenTransactions,
|
||||
) ?: listOf(
|
||||
listConverter.default(
|
||||
userWalletId = userWalletId,
|
||||
fromUserWalletId = fromUserWalletId,
|
||||
toUserWalletId = toUserWalletId,
|
||||
fromCryptoCurrency = fromCryptoCurrency,
|
||||
toCryptoCurrency = toCryptoCurrency,
|
||||
fromAccount = fromAccount,
|
||||
|
|
@ -123,7 +129,7 @@ internal class DefaultSwapTransactionRepository(
|
|||
) { savedTransactions, txStatuses, multiAccountList ->
|
||||
val currencyTxs = savedTransactions
|
||||
?.filter { swapTxList ->
|
||||
swapTxList.userWalletId == userWallet.walletId.stringValue &&
|
||||
swapTxList.fromUserWalletId == userWallet.walletId.stringValue &&
|
||||
(
|
||||
swapTxList.toCryptoCurrencyId == cryptoCurrencyId.value ||
|
||||
swapTxList.fromCryptoCurrencyId == cryptoCurrencyId.value
|
||||
|
|
@ -222,19 +228,27 @@ internal class DefaultSwapTransactionRepository(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun SwapTransactionListDTO.checkId(
|
||||
checkUserWalletId: UserWalletId,
|
||||
fromUserWalletId: UserWalletId,
|
||||
toUserWalletId: UserWalletId,
|
||||
fromAccountId: AccountId?,
|
||||
toAccountId: AccountId?,
|
||||
fromCurrencyId: CryptoCurrency.ID,
|
||||
toCurrencyId: CryptoCurrency.ID,
|
||||
): Boolean {
|
||||
return userWalletId == checkUserWalletId.stringValue &&
|
||||
toCryptoCurrencyId == toCurrencyId.value &&
|
||||
fromCryptoCurrencyId == fromCurrencyId.value
|
||||
return this.fromUserWalletId == fromUserWalletId.stringValue &&
|
||||
this.toUserWalletId == toUserWalletId.stringValue &&
|
||||
this.fromTokensResponse?.accountId == fromAccountId?.value &&
|
||||
this.toTokensResponse?.accountId == toAccountId?.value &&
|
||||
fromCryptoCurrencyId == fromCurrencyId.value &&
|
||||
toCryptoCurrencyId == toCurrencyId.value
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun List<SwapTransactionListDTO>.updateList(
|
||||
userWalletId: UserWalletId,
|
||||
fromUserWalletId: UserWalletId,
|
||||
toUserWalletId: UserWalletId,
|
||||
fromCryptoCurrency: CryptoCurrency,
|
||||
toCryptoCurrency: CryptoCurrency,
|
||||
fromAccount: Account?,
|
||||
|
|
@ -243,7 +257,8 @@ internal class DefaultSwapTransactionRepository(
|
|||
): List<SwapTransactionListDTO> {
|
||||
return addOrReplace(
|
||||
item = listConverter.default(
|
||||
userWalletId = userWalletId,
|
||||
fromUserWalletId = fromUserWalletId,
|
||||
toUserWalletId = toUserWalletId,
|
||||
fromCryptoCurrency = fromCryptoCurrency,
|
||||
toCryptoCurrency = toCryptoCurrency,
|
||||
fromAccount = fromAccount,
|
||||
|
|
@ -252,7 +267,10 @@ internal class DefaultSwapTransactionRepository(
|
|||
),
|
||||
predicate = { swapTxList ->
|
||||
swapTxList.checkId(
|
||||
checkUserWalletId = userWalletId,
|
||||
fromUserWalletId = fromUserWalletId,
|
||||
toUserWalletId = toUserWalletId,
|
||||
fromAccountId = fromAccount?.accountId,
|
||||
toAccountId = toAccount?.accountId,
|
||||
fromCurrencyId = fromCryptoCurrency.id,
|
||||
toCurrencyId = toCryptoCurrency.id,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class TokenInfoConverter : Converter<CryptoCurrency, LeastTokenInfo> {
|
|||
override fun convert(value: CryptoCurrency): LeastTokenInfo {
|
||||
return LeastTokenInfo(
|
||||
contractAddress = (value as? CryptoCurrency.Token)?.contractAddress ?: "0",
|
||||
network = value.network.backendId,
|
||||
network = value.network.rawId,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ internal class SavedSwapTransactionListConverter(
|
|||
}
|
||||
|
||||
override fun convert(value: SwapTransactionListModel) = SwapTransactionListDTO(
|
||||
userWalletId = value.userWalletId,
|
||||
fromUserWalletId = value.fromUserWalletId,
|
||||
toUserWalletId = value.toUserWalletId,
|
||||
fromCryptoCurrencyId = value.fromCryptoCurrencyId,
|
||||
toCryptoCurrencyId = value.toCryptoCurrencyId,
|
||||
fromTokensResponse = userTokensResponseFactory.createResponseToken(
|
||||
|
|
@ -79,7 +80,8 @@ internal class SavedSwapTransactionListConverter(
|
|||
txStatuses = txStatuses,
|
||||
)
|
||||
},
|
||||
userWalletId = value.userWalletId,
|
||||
fromUserWalletId = value.fromUserWalletId,
|
||||
toUserWalletId = value.toUserWalletId,
|
||||
fromCryptoCurrencyId = value.fromCryptoCurrencyId,
|
||||
toCryptoCurrencyId = value.toCryptoCurrencyId,
|
||||
fromCryptoCurrency = fromCryptoCurrency,
|
||||
|
|
@ -98,14 +100,16 @@ internal class SavedSwapTransactionListConverter(
|
|||
|
||||
@Suppress("LongParameterList")
|
||||
fun default(
|
||||
userWalletId: UserWalletId,
|
||||
fromUserWalletId: UserWalletId,
|
||||
toUserWalletId: UserWalletId,
|
||||
fromCryptoCurrency: CryptoCurrency,
|
||||
toCryptoCurrency: CryptoCurrency,
|
||||
fromAccount: Account?,
|
||||
toAccount: Account?,
|
||||
tokenTransactions: List<SwapTransactionDTO>,
|
||||
) = SwapTransactionListDTO(
|
||||
userWalletId = userWalletId.stringValue,
|
||||
fromUserWalletId = fromUserWalletId.stringValue,
|
||||
toUserWalletId = toUserWalletId.stringValue,
|
||||
fromCryptoCurrencyId = fromCryptoCurrency.id.value,
|
||||
toCryptoCurrencyId = toCryptoCurrency.id.value,
|
||||
fromTokensResponse = userTokensResponseFactory.createResponseToken(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ import java.math.BigDecimal
|
|||
@JsonClass(generateAdapter = true)
|
||||
internal data class SwapTransactionListDTO(
|
||||
@Json(name = "userWalletId")
|
||||
val userWalletId: String,
|
||||
val fromUserWalletId: String,
|
||||
@Json(name = "toUserWalletId")
|
||||
val toUserWalletId: String = fromUserWalletId,
|
||||
@Json(name = "fromCryptoCurrencyId")
|
||||
val fromCryptoCurrencyId: String,
|
||||
@Json(name = "toCryptoCurrencyId")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,709 @@
|
|||
package com.tangem.data.swap
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
|
||||
import com.tangem.datasource.api.express.models.request.PairsRequestBody
|
||||
import com.tangem.datasource.api.express.models.response.*
|
||||
import com.tangem.datasource.crypto.DataSignatureVerifier
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.express.ExpressRepository
|
||||
import com.tangem.domain.express.models.*
|
||||
import com.tangem.domain.express.models.ExpressError
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.swap.models.SwapAmountType
|
||||
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
||||
import com.tangem.domain.swap.models.SwapStatus
|
||||
import com.tangem.domain.swap.models.SwapTxType
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import java.math.BigDecimal
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultSwapRepositoryV2Test {
|
||||
|
||||
private val tangemExpressApi: TangemExpressApi = mockk()
|
||||
private val expressRepository: ExpressRepository = mockk()
|
||||
private val appPreferencesStore: AppPreferencesStore = mockk(relaxed = true)
|
||||
private val dataSignatureVerifier: DataSignatureVerifier = mockk()
|
||||
private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier = mockk()
|
||||
private val singleQuoteStatusFetcher: SingleQuoteStatusFetcher = mockk()
|
||||
private val moshi: Moshi = Moshi.Builder().build()
|
||||
|
||||
private val repository = DefaultSwapRepositoryV2(
|
||||
tangemExpressApi = tangemExpressApi,
|
||||
expressRepository = expressRepository,
|
||||
coroutineDispatcher = TestingCoroutineDispatcherProvider(),
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
dataSignatureVerifier = dataSignatureVerifier,
|
||||
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
|
||||
singleQuoteStatusFetcher = singleQuoteStatusFetcher,
|
||||
moshi = moshi,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun resetMocks() {
|
||||
clearMocks(
|
||||
tangemExpressApi,
|
||||
expressRepository,
|
||||
appPreferencesStore,
|
||||
dataSignatureVerifier,
|
||||
singleQuoteStatusSupplier,
|
||||
singleQuoteStatusFetcher,
|
||||
)
|
||||
}
|
||||
|
||||
// region getPairs(SwapCurrencyStatus, SwapCurrencyStatus)
|
||||
|
||||
@Test
|
||||
fun `getPairs with SwapCurrencyStatus returns mapped pairs when providers match`() = runTest {
|
||||
// Arrange
|
||||
val primaryStatus = createCryptoCurrencyStatus(primaryCoin)
|
||||
val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin)
|
||||
val primarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = primaryStatus,
|
||||
account = mockk(),
|
||||
)
|
||||
val secondarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = secondaryStatus,
|
||||
account = mockk(),
|
||||
)
|
||||
|
||||
val swapPair = SwapPair(
|
||||
from = LeastTokenInfo(contractAddress = "0", network = ETH_BACKEND_ID),
|
||||
to = LeastTokenInfo(contractAddress = "0", network = BTC_BACKEND_ID),
|
||||
providers = listOf(SwapPairProvider(providerId = PROVIDER_ID, rateTypes = listOf(RateType.FLOAT))),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getPairs(any(), any(), any())
|
||||
} returns ApiResponse.Success(listOf(swapPair))
|
||||
|
||||
coEvery {
|
||||
expressRepository.getProviders(any(), any())
|
||||
} returns listOf(expressProvider)
|
||||
|
||||
// Act
|
||||
val result = repository.getPairs(
|
||||
primarySwapCurrencyStatus = primarySwapCurrencyStatus,
|
||||
secondarySwapCurrencyStatus = secondarySwapCurrencyStatus,
|
||||
filterProviderTypes = emptyList(),
|
||||
swapTxType = SwapTxType.Swap,
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertThat(result).hasSize(2)
|
||||
assertThat(result.first().from).isEqualTo(primaryStatus)
|
||||
assertThat(result.first().to).isEqualTo(secondaryStatus)
|
||||
assertThat(result.first().providers).hasSize(1)
|
||||
assertThat(result.first().providers.first().providerId).isEqualTo(PROVIDER_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPairs with SwapCurrencyStatus returns empty when no providers match`() = runTest {
|
||||
// Arrange
|
||||
val primaryStatus = createCryptoCurrencyStatus(primaryCoin)
|
||||
val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin)
|
||||
val primarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = primaryStatus,
|
||||
account = mockk(),
|
||||
)
|
||||
val secondarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = secondaryStatus,
|
||||
account = mockk(),
|
||||
)
|
||||
|
||||
val swapPair = SwapPair(
|
||||
from = LeastTokenInfo(contractAddress = "0", network = ETH_BACKEND_ID),
|
||||
to = LeastTokenInfo(contractAddress = "0", network = BTC_BACKEND_ID),
|
||||
providers = listOf(SwapPairProvider(providerId = "unknown-provider", rateTypes = listOf(RateType.FLOAT))),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getPairs(any(), any(), any())
|
||||
} returns ApiResponse.Success(listOf(swapPair))
|
||||
|
||||
coEvery {
|
||||
expressRepository.getProviders(any(), any())
|
||||
} returns listOf(expressProvider)
|
||||
|
||||
// Act
|
||||
val result = repository.getPairs(
|
||||
primarySwapCurrencyStatus = primarySwapCurrencyStatus,
|
||||
secondarySwapCurrencyStatus = secondarySwapCurrencyStatus,
|
||||
filterProviderTypes = emptyList(),
|
||||
swapTxType = SwapTxType.Swap,
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPairs with SwapCurrencyStatus returns empty when API returns empty pairs`() = runTest {
|
||||
// Arrange
|
||||
val primarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = createCryptoCurrencyStatus(primaryCoin),
|
||||
account = mockk(),
|
||||
)
|
||||
val secondarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = createCryptoCurrencyStatus(secondaryCoin),
|
||||
account = mockk(),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getPairs(any(), any(), any())
|
||||
} returns ApiResponse.Success(emptyList())
|
||||
|
||||
coEvery {
|
||||
expressRepository.getProviders(any(), any())
|
||||
} returns listOf(expressProvider)
|
||||
|
||||
// Act
|
||||
val result = repository.getPairs(
|
||||
primarySwapCurrencyStatus = primarySwapCurrencyStatus,
|
||||
secondarySwapCurrencyStatus = secondarySwapCurrencyStatus,
|
||||
filterProviderTypes = emptyList(),
|
||||
swapTxType = SwapTxType.Swap,
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEmpty()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region getPairs(UserWallet, CryptoCurrency, List<CryptoCurrencyStatus>)
|
||||
|
||||
@Test
|
||||
fun `getPairs with currency status list returns mapped pairs`() = runTest {
|
||||
// Arrange
|
||||
val primaryStatus = createCryptoCurrencyStatus(primaryCoin)
|
||||
val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin)
|
||||
|
||||
val swapPair = SwapPair(
|
||||
from = LeastTokenInfo(contractAddress = "0", network = ETH_BACKEND_ID),
|
||||
to = LeastTokenInfo(contractAddress = "0", network = BTC_BACKEND_ID),
|
||||
providers = listOf(SwapPairProvider(providerId = PROVIDER_ID, rateTypes = listOf(RateType.FLOAT))),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getPairs(any(), any(), any())
|
||||
} returns ApiResponse.Success(listOf(swapPair))
|
||||
|
||||
coEvery {
|
||||
expressRepository.getProviders(any(), any())
|
||||
} returns listOf(expressProvider)
|
||||
|
||||
// Act
|
||||
val result = repository.getPairs(
|
||||
userWallet = userWallet,
|
||||
initialCurrency = primaryCoin,
|
||||
cryptoCurrencyStatusList = listOf(primaryStatus, secondaryStatus),
|
||||
filterProviderTypes = emptyList(),
|
||||
swapTxType = SwapTxType.Swap,
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertThat(result).hasSize(2)
|
||||
assertThat(result.first().from).isEqualTo(primaryStatus)
|
||||
assertThat(result.first().to).isEqualTo(secondaryStatus)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPairs with SendWithSwap only fetches forward pairs`() = runTest {
|
||||
// Arrange
|
||||
val primaryStatus = createCryptoCurrencyStatus(primaryCoin)
|
||||
val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin)
|
||||
|
||||
val swapPair = SwapPair(
|
||||
from = LeastTokenInfo(contractAddress = "0", network = ETH_BACKEND_ID),
|
||||
to = LeastTokenInfo(contractAddress = "0", network = BTC_BACKEND_ID),
|
||||
providers = listOf(SwapPairProvider(providerId = PROVIDER_ID, rateTypes = listOf(RateType.FLOAT))),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getPairs(any(), any(), any())
|
||||
} returns ApiResponse.Success(listOf(swapPair))
|
||||
|
||||
coEvery {
|
||||
expressRepository.getProviders(any(), any())
|
||||
} returns listOf(expressProvider)
|
||||
|
||||
// Act
|
||||
val result = repository.getPairs(
|
||||
userWallet = userWallet,
|
||||
initialCurrency = primaryCoin,
|
||||
cryptoCurrencyStatusList = listOf(primaryStatus, secondaryStatus),
|
||||
filterProviderTypes = emptyList(),
|
||||
swapTxType = SwapTxType.SendWithSwap,
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertThat(result).hasSize(1)
|
||||
|
||||
// SendWithSwap should call getPairs only once (forward), not twice (forward + reverse)
|
||||
coVerify(exactly = 1) { tangemExpressApi.getPairs(any(), any(), any()) }
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region getSwapQuote
|
||||
|
||||
@Test
|
||||
fun `getSwapQuote returns correct quote model for fromAmount`() = runTest {
|
||||
// Arrange
|
||||
val quoteResponse = ExchangeQuoteResponse(
|
||||
fromAmount = "1000000000000000000",
|
||||
fromDecimals = 18,
|
||||
toAmount = "100000000",
|
||||
toDecimals = 8,
|
||||
allowanceContract = "0xAllowance",
|
||||
minAmount = BigDecimal.ONE,
|
||||
quoteId = "quote-123",
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getExchangeQuote(
|
||||
fromAmount = any(),
|
||||
toAmount = any(),
|
||||
fromNetwork = any(),
|
||||
fromContractAddress = any(),
|
||||
fromDecimals = any(),
|
||||
toNetwork = any(),
|
||||
toContractAddress = any(),
|
||||
toDecimals = any(),
|
||||
providerId = any(),
|
||||
rateType = any(),
|
||||
userWalletId = any(),
|
||||
refCode = any(),
|
||||
)
|
||||
} returns ApiResponse.Success(quoteResponse)
|
||||
|
||||
// Act
|
||||
val result = repository.getSwapQuote(
|
||||
userWallet = userWallet,
|
||||
fromCryptoCurrency = primaryCoin,
|
||||
toCryptoCurrency = secondaryCoin,
|
||||
amount = BigDecimal.ONE,
|
||||
amountType = SwapAmountType.From,
|
||||
provider = expressProvider,
|
||||
rateType = ExpressRateType.Float,
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertThat(result.provider).isEqualTo(expressProvider)
|
||||
assertThat(result.toTokenAmount).isEqualTo(BigDecimal("1.00000000"))
|
||||
assertThat(result.fromTokenAmount).isEqualTo(BigDecimal("1.000000000000000000"))
|
||||
assertThat(result.allowanceContract).isEqualTo("0xAllowance")
|
||||
assertThat(result.quoteId).isEqualTo("quote-123")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getSwapQuote sends fromAmount when amountType is From`() = runTest {
|
||||
// Arrange
|
||||
val quoteResponse = ExchangeQuoteResponse(
|
||||
fromAmount = "1000000000000000000",
|
||||
fromDecimals = 18,
|
||||
toAmount = "100000000",
|
||||
toDecimals = 8,
|
||||
allowanceContract = null,
|
||||
minAmount = BigDecimal.ONE,
|
||||
quoteId = null,
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getExchangeQuote(
|
||||
fromAmount = any(),
|
||||
toAmount = any(),
|
||||
fromNetwork = any(),
|
||||
fromContractAddress = any(),
|
||||
fromDecimals = any(),
|
||||
toNetwork = any(),
|
||||
toContractAddress = any(),
|
||||
toDecimals = any(),
|
||||
providerId = any(),
|
||||
rateType = any(),
|
||||
userWalletId = any(),
|
||||
refCode = any(),
|
||||
)
|
||||
} returns ApiResponse.Success(quoteResponse)
|
||||
|
||||
// Act
|
||||
repository.getSwapQuote(
|
||||
userWallet = userWallet,
|
||||
fromCryptoCurrency = primaryCoin,
|
||||
toCryptoCurrency = secondaryCoin,
|
||||
amount = BigDecimal("2.5"),
|
||||
amountType = SwapAmountType.From,
|
||||
provider = expressProvider,
|
||||
rateType = ExpressRateType.Float,
|
||||
)
|
||||
|
||||
// Assert — fromAmount is set, toAmount is null
|
||||
coVerify {
|
||||
tangemExpressApi.getExchangeQuote(
|
||||
fromAmount = "2500000000000000000",
|
||||
toAmount = null,
|
||||
fromNetwork = ETH_BACKEND_ID,
|
||||
fromContractAddress = "0",
|
||||
fromDecimals = 18,
|
||||
toNetwork = BTC_BACKEND_ID,
|
||||
toContractAddress = "0",
|
||||
toDecimals = 8,
|
||||
providerId = PROVIDER_ID,
|
||||
rateType = "float",
|
||||
userWalletId = any(),
|
||||
refCode = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getSwapQuote sends toAmount when amountType is To`() = runTest {
|
||||
// Arrange
|
||||
val quoteResponse = ExchangeQuoteResponse(
|
||||
fromAmount = "1000000000000000000",
|
||||
fromDecimals = 18,
|
||||
toAmount = "100000000",
|
||||
toDecimals = 8,
|
||||
allowanceContract = null,
|
||||
minAmount = BigDecimal.ONE,
|
||||
quoteId = null,
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getExchangeQuote(
|
||||
fromAmount = any(),
|
||||
toAmount = any(),
|
||||
fromNetwork = any(),
|
||||
fromContractAddress = any(),
|
||||
fromDecimals = any(),
|
||||
toNetwork = any(),
|
||||
toContractAddress = any(),
|
||||
toDecimals = any(),
|
||||
providerId = any(),
|
||||
rateType = any(),
|
||||
userWalletId = any(),
|
||||
refCode = any(),
|
||||
)
|
||||
} returns ApiResponse.Success(quoteResponse)
|
||||
|
||||
// Act
|
||||
repository.getSwapQuote(
|
||||
userWallet = userWallet,
|
||||
fromCryptoCurrency = primaryCoin,
|
||||
toCryptoCurrency = secondaryCoin,
|
||||
amount = BigDecimal("1.5"),
|
||||
amountType = SwapAmountType.To,
|
||||
provider = expressProvider,
|
||||
rateType = ExpressRateType.Fixed,
|
||||
)
|
||||
|
||||
// Assert — toAmount is set, fromAmount is null
|
||||
coVerify {
|
||||
tangemExpressApi.getExchangeQuote(
|
||||
fromAmount = null,
|
||||
toAmount = "150000000",
|
||||
fromNetwork = ETH_BACKEND_ID,
|
||||
fromContractAddress = "0",
|
||||
fromDecimals = 18,
|
||||
toNetwork = BTC_BACKEND_ID,
|
||||
toContractAddress = "0",
|
||||
toDecimals = 8,
|
||||
providerId = PROVIDER_ID,
|
||||
rateType = "fixed",
|
||||
userWalletId = any(),
|
||||
refCode = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region getExchangeStatus
|
||||
|
||||
@Test
|
||||
fun `getExchangeStatus returns converted status model`() = runTest {
|
||||
// Arrange
|
||||
val statusResponse = ExchangeStatusResponse(
|
||||
providerId = PROVIDER_ID,
|
||||
status = ExchangeStatus.Finished,
|
||||
externalTxId = "ext-tx-1",
|
||||
externalTxUrl = "https://example.com/tx/1",
|
||||
error = null,
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getExchangeStatus(any(), any(), any())
|
||||
} returns ApiResponse.Success(statusResponse)
|
||||
|
||||
// Act
|
||||
val result = repository.getExchangeStatus(userWallet = userWallet, txId = "tx-123")
|
||||
|
||||
// Assert
|
||||
assertThat(result.providerId).isEqualTo(PROVIDER_ID)
|
||||
assertThat(result.status).isEqualTo(SwapStatus.Finished)
|
||||
assertThat(result.txId).isEqualTo("ext-tx-1")
|
||||
assertThat(result.txExternalUrl).isEqualTo("https://example.com/tx/1")
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region swapTransactionSent
|
||||
|
||||
@Test
|
||||
fun `swapTransactionSent calls exchangeSent API`() = runTest {
|
||||
// Arrange
|
||||
val fromStatus = createCryptoCurrencyStatus(primaryCoin)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.exchangeSent(any(), any(), any())
|
||||
} returns ApiResponse.Success(ExchangeSentResponseBody(txId = "tx-1", status = "ok"))
|
||||
|
||||
// Act
|
||||
repository.swapTransactionSent(
|
||||
userWallet = userWallet,
|
||||
fromCryptoCurrencyStatus = fromStatus,
|
||||
payInAddress = "0xPayIn",
|
||||
txId = "tx-1",
|
||||
txHash = "0xHash",
|
||||
txExtraId = null,
|
||||
)
|
||||
|
||||
// Assert
|
||||
coVerify {
|
||||
tangemExpressApi.exchangeSent(
|
||||
userWalletId = any(),
|
||||
refCode = any(),
|
||||
body = match { body ->
|
||||
body.txId == "tx-1" &&
|
||||
body.txHash == "0xHash" &&
|
||||
body.payinAddress == "0xPayIn" &&
|
||||
body.payinExtraId == null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region filterYieldSupplyProvider
|
||||
|
||||
@Test
|
||||
fun `getPairs filters out DEX providers when yield supply is active`() = runTest {
|
||||
// Arrange
|
||||
val primaryStatus = createCryptoCurrencyStatusWithActiveYield(primaryCoin)
|
||||
val secondaryStatus = createCryptoCurrencyStatus(secondaryCoin)
|
||||
val primarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = primaryStatus,
|
||||
account = mockk(),
|
||||
)
|
||||
val secondarySwapCurrencyStatus = SwapCurrencyStatus(
|
||||
userWallet = userWallet,
|
||||
status = secondaryStatus,
|
||||
account = mockk(),
|
||||
)
|
||||
|
||||
val swapPair = SwapPair(
|
||||
from = LeastTokenInfo(contractAddress = "0", network = ETH_BACKEND_ID),
|
||||
to = LeastTokenInfo(contractAddress = "0", network = BTC_BACKEND_ID),
|
||||
providers = listOf(
|
||||
SwapPairProvider(providerId = PROVIDER_ID, rateTypes = listOf(RateType.FLOAT)),
|
||||
SwapPairProvider(providerId = CEX_PROVIDER_ID, rateTypes = listOf(RateType.FLOAT)),
|
||||
),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getPairs(any(), any(), any())
|
||||
} returns ApiResponse.Success(listOf(swapPair))
|
||||
|
||||
coEvery {
|
||||
expressRepository.getProviders(any(), any())
|
||||
} returns listOf(dexProvider, cexProvider)
|
||||
|
||||
// Act
|
||||
val result = repository.getPairs(
|
||||
primarySwapCurrencyStatus = primarySwapCurrencyStatus,
|
||||
secondarySwapCurrencyStatus = secondarySwapCurrencyStatus,
|
||||
filterProviderTypes = emptyList(),
|
||||
swapTxType = SwapTxType.Swap,
|
||||
)
|
||||
|
||||
// Assert — only CEX provider should remain
|
||||
assertThat(result).hasSize(2)
|
||||
val providers = result.first().providers
|
||||
assertThat(providers).hasSize(1)
|
||||
assertThat(providers.first().type).isEqualTo(ExpressProviderType.CEX)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region getSwapData
|
||||
|
||||
@Test
|
||||
fun `getSwapData throws InvalidSignatureError when signature verification fails`() = runTest {
|
||||
// Arrange
|
||||
val fromStatus = createCryptoCurrencyStatus(primaryCoin)
|
||||
val exchangeDataResponse = ExchangeDataResponse(
|
||||
fromAmount = "1000000000000000000",
|
||||
fromDecimals = 18,
|
||||
toAmount = "100000000",
|
||||
toDecimals = 8,
|
||||
txId = "tx-123",
|
||||
txDetailsJson = "{}",
|
||||
signature = "invalid-sig",
|
||||
)
|
||||
|
||||
coEvery {
|
||||
tangemExpressApi.getExchangeData(
|
||||
fromContractAddress = any(),
|
||||
toContractAddress = any(),
|
||||
fromNetwork = any(),
|
||||
toNetwork = any(),
|
||||
fromAddress = any(),
|
||||
toAddress = any(),
|
||||
fromDecimals = any(),
|
||||
toDecimals = any(),
|
||||
fromAmount = any(),
|
||||
toAmount = any(),
|
||||
providerId = any(),
|
||||
rateType = any(),
|
||||
requestId = any(),
|
||||
refundAddress = any(),
|
||||
refundExtraId = any(),
|
||||
userWalletId = any(),
|
||||
partnerOperationType = any(),
|
||||
refCode = any(),
|
||||
toExtraId = any(),
|
||||
quoteId = any(),
|
||||
)
|
||||
} returns ApiResponse.Success(exchangeDataResponse)
|
||||
|
||||
every { dataSignatureVerifier.verifySignature(any(), any()) } returns false
|
||||
|
||||
// Act & Assert
|
||||
assertThrows<ExpressError.InvalidSignatureError> {
|
||||
repository.getSwapData(
|
||||
userWallet = userWallet,
|
||||
fromCryptoCurrencyStatus = fromStatus,
|
||||
toCryptoCurrency = secondaryCoin,
|
||||
amount = BigDecimal.ONE,
|
||||
amountType = SwapAmountType.From,
|
||||
toAddress = "0xToAddress",
|
||||
toExtraId = null,
|
||||
expressProvider = cexProvider,
|
||||
rateType = ExpressRateType.Float,
|
||||
expressOperationType = ExpressOperationType.SWAP,
|
||||
quoteId = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
private companion object {
|
||||
const val ETH_BACKEND_ID = "ethereum"
|
||||
const val BTC_BACKEND_ID = "bitcoin"
|
||||
const val PROVIDER_ID = "dex-provider-1"
|
||||
const val CEX_PROVIDER_ID = "cex-provider-1"
|
||||
|
||||
val userWallet: UserWallet = MockUserWalletFactory.create()
|
||||
|
||||
val ethNetwork: Network = mockk(relaxed = true) {
|
||||
every { rawId } returns ETH_BACKEND_ID
|
||||
}
|
||||
|
||||
val btcNetwork: Network = mockk(relaxed = true) {
|
||||
every { rawId } returns BTC_BACKEND_ID
|
||||
}
|
||||
|
||||
val primaryCoin: CryptoCurrency.Coin = mockk(relaxed = true) {
|
||||
every { network } returns ethNetwork
|
||||
every { decimals } returns 18
|
||||
every { id } returns mockk(relaxed = true) {
|
||||
every { rawCurrencyId } returns CryptoCurrency.RawID("ethereum")
|
||||
}
|
||||
}
|
||||
|
||||
val secondaryCoin: CryptoCurrency.Coin = mockk(relaxed = true) {
|
||||
every { network } returns btcNetwork
|
||||
every { decimals } returns 8
|
||||
every { id } returns mockk(relaxed = true) {
|
||||
every { rawCurrencyId } returns CryptoCurrency.RawID("bitcoin")
|
||||
}
|
||||
}
|
||||
|
||||
val expressProvider = ExpressProvider(
|
||||
providerId = PROVIDER_ID,
|
||||
rateTypes = listOf(ExpressRateType.Float),
|
||||
name = "Test DEX",
|
||||
type = ExpressProviderType.DEX,
|
||||
imageLarge = "",
|
||||
termsOfUse = null,
|
||||
privacyPolicy = null,
|
||||
slippage = null,
|
||||
)
|
||||
|
||||
val dexProvider = expressProvider
|
||||
|
||||
val cexProvider = ExpressProvider(
|
||||
providerId = CEX_PROVIDER_ID,
|
||||
rateTypes = listOf(ExpressRateType.Float),
|
||||
name = "Test CEX",
|
||||
type = ExpressProviderType.CEX,
|
||||
imageLarge = "",
|
||||
termsOfUse = null,
|
||||
privacyPolicy = null,
|
||||
slippage = null,
|
||||
)
|
||||
|
||||
fun createCryptoCurrencyStatus(currency: CryptoCurrency): CryptoCurrencyStatus {
|
||||
val value: CryptoCurrencyStatus.Value = mockk(relaxed = true) {
|
||||
every { yieldSupplyStatus } returns null
|
||||
every { networkAddress } returns mockk(relaxed = true) {
|
||||
every { defaultAddress } returns mockk(relaxed = true) {
|
||||
every { value } returns "0xAddress"
|
||||
}
|
||||
}
|
||||
}
|
||||
return CryptoCurrencyStatus(currency = currency, value = value)
|
||||
}
|
||||
|
||||
fun createCryptoCurrencyStatusWithActiveYield(currency: CryptoCurrency): CryptoCurrencyStatus {
|
||||
val yieldStatus: YieldSupplyStatus = mockk {
|
||||
every { isActive } returns true
|
||||
}
|
||||
val value: CryptoCurrencyStatus.Value = mockk(relaxed = true) {
|
||||
every { yieldSupplyStatus } returns yieldStatus
|
||||
every { networkAddress } returns mockk(relaxed = true) {
|
||||
every { defaultAddress } returns mockk(relaxed = true) {
|
||||
every { value } returns "0xAddress"
|
||||
}
|
||||
}
|
||||
}
|
||||
return CryptoCurrencyStatus(currency = currency, value = value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -134,7 +134,7 @@ internal class DefaultCurrenciesRepository(
|
|||
}
|
||||
|
||||
override fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean {
|
||||
val blockchain = Blockchain.fromNetworkId(network.backendId)
|
||||
val blockchain = Blockchain.fromNetworkId(network.rawId)
|
||||
return blockchain?.isNetworkFeeZero() == true
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,11 @@ package com.tangem.data.tokens.repository
|
|||
|
||||
import com.tangem.blockchain.blockchains.ethereum.eip1559.isGaslessTxSupported
|
||||
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.common.FeeResourceAmountProvider
|
||||
import com.tangem.blockchain.common.MinimumSendAmountProvider
|
||||
import com.tangem.blockchain.common.ReserveAmountProvider
|
||||
import com.tangem.blockchain.common.UtxoAmountLimitProvider
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.common.getTotalStakingBalance
|
||||
import com.tangem.data.tokens.converters.UtxoConverter
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
@ -67,7 +71,7 @@ internal class DefaultCurrencyChecksRepository(
|
|||
}
|
||||
|
||||
override fun isNetworkSupportedForGaslessTx(network: Network): Boolean {
|
||||
val blockchain = Blockchain.fromId(network.rawId)
|
||||
val blockchain = network.toBlockchain()
|
||||
return blockchain.isGaslessTxSupported
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
package com.tangem.data.tokensync.di
|
||||
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.tokensync.repository.DefaultTokenSyncRepository
|
||||
import com.tangem.data.tokensync.store.TokenSyncStoreFactory
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.tokensync.repository.TokenSyncRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object TokenSyncDataModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTokenSyncRepository(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
tangemTechApi: TangemTechApi,
|
||||
userWalletsListRepository: UserWalletsListRepository,
|
||||
networkFactory: NetworkFactory,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
tokenSyncStoreFactory: TokenSyncStoreFactory,
|
||||
responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
): TokenSyncRepository {
|
||||
return DefaultTokenSyncRepository(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
tangemTechApi = tangemTechApi,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
networkFactory = networkFactory,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
tokenSyncStoreFactory = tokenSyncStoreFactory,
|
||||
responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory,
|
||||
dispatchers = dispatchers,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ internal class DefaultAllowanceRepository(
|
|||
allowance >= requiredAmount -> AllowanceInfo.Enough(allowance)
|
||||
allowance > BigDecimal.ZERO && allowance < requiredAmount &&
|
||||
BlockchainUtils.isTetherInEthereum(
|
||||
blockchainId = cryptoCurrency.network.rawId,
|
||||
networkId = cryptoCurrency.network.rawId,
|
||||
contractAddress = cryptoCurrency.contractAddress,
|
||||
) -> AllowanceInfo.ResetNeeded(allowance, requiredAmount)
|
||||
else -> AllowanceInfo.NotEnough(allowance, requiredAmount)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.data.transaction
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.data.transaction.convertes.GaslessSignedTransactionResultConverter
|
||||
import com.tangem.data.transaction.convertes.GaslessTransactionRequestBuilder
|
||||
|
|
@ -45,7 +45,7 @@ class DefaultGaslessTransactionRepository(
|
|||
|
||||
val supportedTokensData = gaslessTxServiceApi.getSupportedTokens().getOrThrow()
|
||||
if (supportedTokensData.isSuccess) {
|
||||
val networkBlockchain = Blockchain.fromId(network.rawId)
|
||||
val networkBlockchain = network.toBlockchain()
|
||||
val supportedTokens = supportedTokensData.result.tokens
|
||||
.filter {
|
||||
it.chainId == networkBlockchain.getChainId()
|
||||
|
|
@ -100,7 +100,7 @@ class DefaultGaslessTransactionRepository(
|
|||
network: Network,
|
||||
eip7702Auth: Eip7702Authorization?,
|
||||
): GaslessSignedTransactionResult = withContext(coroutineDispatcherProvider.io) {
|
||||
val blockchain = Blockchain.fromId(network.rawId)
|
||||
val blockchain = network.toBlockchain()
|
||||
val transactionRequest = gaslessTransactionRequestBuilder.build(
|
||||
gaslessTransaction = gaslessTransactionData,
|
||||
signature = signature,
|
||||
|
|
@ -124,7 +124,7 @@ class DefaultGaslessTransactionRepository(
|
|||
}
|
||||
|
||||
override fun getChainIdForNetwork(network: Network): Int {
|
||||
val networkBlockchain = Blockchain.fromId(network.rawId)
|
||||
val networkBlockchain = network.toBlockchain()
|
||||
return networkBlockchain.getChainId() ?: error("ChainId not found for blockchain ${networkBlockchain.name}")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ internal class DefaultTransactionRepository(
|
|||
derivationPath = network.derivationPath.value,
|
||||
) ?: error("Wallet manager not found")
|
||||
|
||||
val extras = txExtras ?: getMemoExtras(networkId = network.rawId, memo)
|
||||
val extras = txExtras ?: getMemoExtras(networkId = network.id.rawId, memo)
|
||||
|
||||
val patchedDestination = if (amount.type is AmountType.TokenYieldSupply) {
|
||||
walletManager.getYieldModuleAddress()
|
||||
|
|
@ -128,7 +128,7 @@ internal class DefaultTransactionRepository(
|
|||
destination = destination,
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
txExtras = getMemoExtras(networkId = network.rawId, memo = memo) ?: extras,
|
||||
txExtras = getMemoExtras(networkId = network.id.rawId, memo = memo) ?: extras,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -262,7 +262,7 @@ internal class DefaultTransactionRepository(
|
|||
fee = fee ?: Fee.Common(amount = amount),
|
||||
destination = destination,
|
||||
).copy(
|
||||
extras = getMemoExtras(networkId = network.rawId, memo = memo),
|
||||
extras = getMemoExtras(networkId = network.id.rawId, memo = memo),
|
||||
)
|
||||
|
||||
validator.validate(transactionData = transactionData)
|
||||
|
|
@ -312,7 +312,7 @@ internal class DefaultTransactionRepository(
|
|||
nonce: BigInteger?,
|
||||
gasLimit: BigInteger?,
|
||||
): TransactionExtras {
|
||||
val blockchain = Blockchain.fromNetworkId(networkId = network.backendId)
|
||||
val blockchain = Blockchain.fromNetworkId(networkId = network.rawId)
|
||||
?: error("Blockchain not found")
|
||||
return when {
|
||||
blockchain.isEvm() -> {
|
||||
|
|
@ -332,8 +332,8 @@ internal class DefaultTransactionRepository(
|
|||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? {
|
||||
val blockchain = Blockchain.fromId(networkId)
|
||||
private fun getMemoExtras(networkId: Network.RawID, memo: String?): TransactionExtras? {
|
||||
val blockchain = networkId.toBlockchain()
|
||||
if (memo == null) return null
|
||||
return when (blockchain) {
|
||||
Blockchain.Stellar -> {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.data.transaction
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
|
|
@ -30,7 +31,7 @@ class MockedGaslessTransactionRepository(
|
|||
}
|
||||
|
||||
override fun getChainIdForNetwork(network: Network): Int {
|
||||
val networkBlockchain = Blockchain.fromId(network.rawId)
|
||||
val networkBlockchain = network.toBlockchain()
|
||||
return networkBlockchain.getChainId() ?: error("ChainId not found for blockchain ${networkBlockchain.name}")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -171,7 +171,11 @@ class DefaultAllowanceRepositoryTest {
|
|||
|
||||
@Test
|
||||
fun `returns NotEnough when partial allowance for non-tether token`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "ethereum", rawCurrencyId = "usd-coin")
|
||||
val token = buildToken(
|
||||
rawNetworkId = "ethereum",
|
||||
rawCurrencyId = "usd-coin",
|
||||
contractAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
)
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
|
|
@ -187,7 +191,7 @@ class DefaultAllowanceRepositoryTest {
|
|||
|
||||
@Test
|
||||
fun `returns NotEnough when partial allowance for tether on non-ethereum network`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "polygon", rawCurrencyId = "tether")
|
||||
val token = buildToken(rawNetworkId = "polygon-pos", rawCurrencyId = "tether")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
|
|
@ -200,7 +204,7 @@ class DefaultAllowanceRepositoryTest {
|
|||
|
||||
@Test
|
||||
fun `returns ResetNeeded when partial allowance for tether on ethereum`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "ETH", rawCurrencyId = "tether")
|
||||
val token = buildToken(rawNetworkId = "ethereum", rawCurrencyId = "tether")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
|
|
@ -216,7 +220,7 @@ class DefaultAllowanceRepositoryTest {
|
|||
|
||||
@Test
|
||||
fun `returns ResetNeeded when partial allowance for tether on ethereum testnet`() = runTest {
|
||||
val token = buildToken(rawNetworkId = "ETH/test", rawCurrencyId = "tether")
|
||||
val token = buildToken(rawNetworkId = "ethereum/test", rawCurrencyId = "tether")
|
||||
|
||||
coEvery {
|
||||
(approverWalletManager as Approver).getAllowance(spenderAddress, any())
|
||||
|
|
@ -248,8 +252,7 @@ class DefaultAllowanceRepositoryTest {
|
|||
private fun buildNetwork(rawNetworkId: String): Network {
|
||||
val derivationPath = Network.DerivationPath.None
|
||||
return Network(
|
||||
id = Network.ID(Network.RawID(rawNetworkId), derivationPath),
|
||||
backendId = rawNetworkId,
|
||||
id = Network.ID(value = rawNetworkId, derivationPath = derivationPath),
|
||||
name = rawNetworkId.replaceFirstChar { it.uppercase() },
|
||||
currencySymbol = "ETH",
|
||||
derivationPath = derivationPath,
|
||||
|
|
@ -263,7 +266,7 @@ class DefaultAllowanceRepositoryTest {
|
|||
}
|
||||
|
||||
private fun buildToken(
|
||||
rawNetworkId: String = "ETH",
|
||||
rawNetworkId: String = "ethereum",
|
||||
rawCurrencyId: String = "tether",
|
||||
contractAddress: String = "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
): CryptoCurrency.Token {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
|||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
|
||||
import com.tangem.data.pay.util.TangemPayErrorConverter
|
||||
import com.tangem.domain.card.common.visa.VisaUtilities
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
|
||||
|
|
@ -16,14 +16,8 @@ import com.tangem.utils.logging.TangemLogger
|
|||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "TangemPay: DefaultTangemPayCryptoCurrencyFactory"
|
||||
/**
|
||||
* Custom token parameters. Will be used only for F&F.
|
||||
*/
|
||||
private const val TOKEN_ID = "usd-coin"
|
||||
private const val TOKEN_NAME = "USDC"
|
||||
private const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
|
||||
private const val TOKEN_DECIMALS = 6
|
||||
|
||||
@Deprecated("Use TangemPayCurrencyFactory instead")
|
||||
internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor(
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
private val errorConverter: TangemPayErrorConverter,
|
||||
|
|
@ -47,32 +41,11 @@ internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor(
|
|||
)
|
||||
cryptoCurrencyFactory.createToken(
|
||||
network = requireNotNull(network),
|
||||
rawId = CryptoCurrency.RawID(TOKEN_ID),
|
||||
name = TOKEN_NAME,
|
||||
symbol = TOKEN_NAME,
|
||||
contractAddress = TOKEN_CONTRACT_ADDRESS,
|
||||
decimals = TOKEN_DECIMALS,
|
||||
)
|
||||
}.mapLeft { exception ->
|
||||
TangemLogger.withTag(TAG).e("Error", exception)
|
||||
errorConverter.convert(exception)
|
||||
}
|
||||
}
|
||||
|
||||
override fun create(userWallet: UserWallet): Either<UniversalError, CryptoCurrency.Token> {
|
||||
return catch {
|
||||
val network = networkFactory.create(
|
||||
blockchain = VisaUtilities.visaBlockchain,
|
||||
extraDerivationPath = null,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
cryptoCurrencyFactory.createToken(
|
||||
network = requireNotNull(network),
|
||||
rawId = CryptoCurrency.RawID(TOKEN_ID),
|
||||
name = TOKEN_NAME,
|
||||
symbol = TOKEN_NAME,
|
||||
contractAddress = TOKEN_CONTRACT_ADDRESS,
|
||||
decimals = TOKEN_DECIMALS,
|
||||
rawId = CryptoCurrency.RawID(TangemPayCurrencyFactory.TOKEN_ID),
|
||||
name = TangemPayCurrencyFactory.TOKEN_NAME,
|
||||
symbol = TangemPayCurrencyFactory.TOKEN_NAME,
|
||||
contractAddress = TangemPayCurrencyFactory.TOKEN_CONTRACT_ADDRESS,
|
||||
decimals = TangemPayCurrencyFactory.TOKEN_DECIMALS,
|
||||
)
|
||||
}.mapLeft { exception ->
|
||||
TangemLogger.withTag(TAG).e("Error", exception)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.data.pay
|
|||
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.TangemPayEligibilityType
|
||||
import com.tangem.domain.models.pay.TangemPayEligibilityType
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
package com.tangem.data.pay.converter
|
||||
|
||||
import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter.convert
|
||||
import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter.convertBack
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
|
||||
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.account.CardDisplayName
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.utils.converter.TwoWayConverter
|
||||
import com.tangem.domain.models.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardLimit
|
||||
import com.tangem.domain.models.pay.TangemPayCardLimitData
|
||||
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Two-way converter between [PaymentAccountStatusValue] and [PaymentAccountStatusValueDM].
|
||||
|
|
@ -15,10 +22,12 @@ import com.tangem.utils.converter.TwoWayConverter
|
|||
*
|
||||
* [convertBack] maps data model → domain. All restored statuses have [StatusSource.CACHE] as source.
|
||||
*/
|
||||
internal object PaymentAccountStatusValueDMConverter :
|
||||
TwoWayConverter<PaymentAccountStatusValue, PaymentAccountStatusValueDM?> {
|
||||
@Singleton
|
||||
internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
||||
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory,
|
||||
) {
|
||||
|
||||
override fun convert(value: PaymentAccountStatusValue): PaymentAccountStatusValueDM? {
|
||||
fun convert(value: PaymentAccountStatusValue): PaymentAccountStatusValueDM? {
|
||||
return when (value) {
|
||||
is PaymentAccountStatusValue.NotCreated -> PaymentAccountStatusValueDM.NotCreated()
|
||||
is PaymentAccountStatusValue.UnderReview -> PaymentAccountStatusValueDM.UnderReview(
|
||||
|
|
@ -26,27 +35,23 @@ internal object PaymentAccountStatusValueDMConverter :
|
|||
customerId = value.customerId,
|
||||
)
|
||||
is PaymentAccountStatusValue.IssuingCard -> PaymentAccountStatusValueDM.IssuingCard()
|
||||
is PaymentAccountStatusValue.Locked -> PaymentAccountStatusValueDM.ActiveCard(
|
||||
isLocked = true,
|
||||
is PaymentAccountStatusValue.Loaded -> PaymentAccountStatusValueDM.ActiveAccount(
|
||||
customerId = value.customerId,
|
||||
cardId = value.cardId,
|
||||
lastFourDigits = value.lastFourDigits,
|
||||
currencyCode = value.currencyCode,
|
||||
depositAddress = value.depositAddress,
|
||||
isPinSet = value.isPinSet,
|
||||
fiatBalance = value.fiatBalance.toDM(),
|
||||
cryptoBalance = value.cryptoBalance.toDM(),
|
||||
)
|
||||
is PaymentAccountStatusValue.Loaded -> PaymentAccountStatusValueDM.ActiveCard(
|
||||
isLocked = false,
|
||||
customerId = value.customerId,
|
||||
cardId = value.cardId,
|
||||
lastFourDigits = value.lastFourDigits,
|
||||
currencyCode = value.currencyCode,
|
||||
depositAddress = value.depositAddress,
|
||||
isPinSet = value.isPinSet,
|
||||
fiatBalance = value.fiatBalance.toDM(),
|
||||
cryptoBalance = value.cryptoBalance.toDM(),
|
||||
cards = value.cards.map { card ->
|
||||
PaymentAccountStatusValueDM.TangemPayCard(
|
||||
id = card.id,
|
||||
hasPinCode = card.hasPinCode,
|
||||
displayName = card.displayName?.value,
|
||||
actualDailyLimit = card.limit?.actualCardLimit?.amount,
|
||||
adminDailyLimit = card.limit?.adminCardLimit?.amount,
|
||||
isFrozen = card.isFrozen,
|
||||
lastDigits = card.lastDigits,
|
||||
)
|
||||
},
|
||||
)
|
||||
is PaymentAccountStatusValue.Error.CardIssueFailed -> PaymentAccountStatusValueDM.CardIssueFailed(
|
||||
customerId = value.customerId,
|
||||
|
|
@ -61,7 +66,7 @@ internal object PaymentAccountStatusValueDMConverter :
|
|||
}
|
||||
}
|
||||
|
||||
override fun convertBack(value: PaymentAccountStatusValueDM?): PaymentAccountStatusValue {
|
||||
fun convertBack(userWalletId: UserWalletId, value: PaymentAccountStatusValueDM?): PaymentAccountStatusValue {
|
||||
return when (value) {
|
||||
is PaymentAccountStatusValueDM.Empty -> PaymentAccountStatusValue.Empty
|
||||
is PaymentAccountStatusValueDM.NotCreated -> PaymentAccountStatusValue.NotCreated
|
||||
|
|
@ -71,31 +76,32 @@ internal object PaymentAccountStatusValueDMConverter :
|
|||
is PaymentAccountStatusValueDM.IssuingCard -> PaymentAccountStatusValue.IssuingCard(
|
||||
source = StatusSource.CACHE,
|
||||
)
|
||||
is PaymentAccountStatusValueDM.ActiveCard -> if (value.isLocked) {
|
||||
PaymentAccountStatusValue.Locked(
|
||||
source = StatusSource.CACHE,
|
||||
customerId = value.customerId,
|
||||
cardId = value.cardId,
|
||||
lastFourDigits = value.lastFourDigits,
|
||||
currencyCode = value.currencyCode,
|
||||
depositAddress = value.depositAddress,
|
||||
isPinSet = value.isPinSet,
|
||||
fiatBalance = value.fiatBalance.toDomain(),
|
||||
cryptoBalance = value.cryptoBalance.toDomain(),
|
||||
)
|
||||
} else {
|
||||
PaymentAccountStatusValue.Loaded(
|
||||
source = StatusSource.CACHE,
|
||||
customerId = value.customerId,
|
||||
cardId = value.cardId,
|
||||
lastFourDigits = value.lastFourDigits,
|
||||
currencyCode = value.currencyCode,
|
||||
depositAddress = value.depositAddress,
|
||||
isPinSet = value.isPinSet,
|
||||
fiatBalance = value.fiatBalance.toDomain(),
|
||||
cryptoBalance = value.cryptoBalance.toDomain(),
|
||||
)
|
||||
}
|
||||
is PaymentAccountStatusValueDM.ActiveAccount -> PaymentAccountStatusValue.Loaded(
|
||||
source = StatusSource.CACHE,
|
||||
customerId = value.customerId,
|
||||
currencyCode = value.currencyCode,
|
||||
depositAddress = value.depositAddress,
|
||||
fiatBalance = value.fiatBalance.toDomain(),
|
||||
cryptoBalance = value.cryptoBalance.toDomain(),
|
||||
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
|
||||
cards = value.cards.map { card ->
|
||||
TangemPayCard(
|
||||
id = card.id,
|
||||
hasPinCode = card.hasPinCode,
|
||||
displayName = card.displayName?.let { CardDisplayName(it).getOrElse { null } },
|
||||
limit = TangemPayCardLimitData(
|
||||
actualCardLimit = card.actualDailyLimit?.let { limit ->
|
||||
TangemPayCardLimit(limit, TangemPayCardLimitPeriod.DAY)
|
||||
},
|
||||
adminCardLimit = card.adminDailyLimit?.let { limit ->
|
||||
TangemPayCardLimit(limit, TangemPayCardLimitPeriod.DAY)
|
||||
},
|
||||
),
|
||||
isFrozen = card.isFrozen,
|
||||
lastDigits = card.lastDigits,
|
||||
)
|
||||
},
|
||||
)
|
||||
is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview(
|
||||
source = StatusSource.CACHE,
|
||||
kycStatus = value.kycStatus,
|
||||
|
|
|
|||
|
|
@ -2,13 +2,16 @@ package com.tangem.data.pay.di
|
|||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory
|
||||
import com.tangem.data.pay.DefaultTangemPayEligibilityManager
|
||||
import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter
|
||||
import com.tangem.data.pay.flow.DefaultPaymentAccountStatusFetcher
|
||||
import com.tangem.data.pay.flow.DefaultPaymentAccountStatusProducer
|
||||
import com.tangem.data.pay.repository.*
|
||||
import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
|
||||
import com.tangem.data.pay.store.PaymentAccountStatusesStore
|
||||
import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.data.pay.usecase.DefaultGetTangemPayCustomerIdUseCase
|
||||
|
|
@ -24,7 +27,9 @@ import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
|||
import com.tangem.domain.pay.flow.PaymentAccountStatusProducer
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import com.tangem.domain.pay.repository.*
|
||||
import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
|
||||
import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase
|
||||
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
|
||||
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
|
||||
|
|
@ -69,6 +74,10 @@ internal interface TangemPayDataModule {
|
|||
@Singleton
|
||||
fun bindCustomerOrderRepository(repository: DefaultCustomerOrderRepository): CustomerOrderRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindReissueCardRepository(repository: DefaultReissueCardRepository): TangemPayReissueCardRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayCryptoCurrencyFactory(
|
||||
|
|
@ -112,6 +121,7 @@ internal interface TangemPayDataModule {
|
|||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
scope: AppCoroutineScope,
|
||||
converter: PaymentAccountStatusValueDMConverter,
|
||||
): PaymentAccountStatusesStore {
|
||||
return PaymentAccountStatusesStore(
|
||||
runtimeStore = RuntimeSharedStore(),
|
||||
|
|
@ -121,9 +131,11 @@ internal interface TangemPayDataModule {
|
|||
types = mapWithStringKeyTypes<PaymentAccountStatusValueDM>(),
|
||||
defaultValue = emptyMap(),
|
||||
),
|
||||
corruptionHandler = ReplaceFileCorruptionHandler { emptyMap() },
|
||||
produceFile = { context.dataStoreFile(fileName = "payment_account_statuses") },
|
||||
scope = scope,
|
||||
),
|
||||
converter = converter,
|
||||
scope = scope,
|
||||
)
|
||||
}
|
||||
|
|
@ -139,6 +151,22 @@ internal interface TangemPayDataModule {
|
|||
) {}
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideSetTangemPayCardLimitUseCase(
|
||||
cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
): SetTangemPayCardLimitUseCase {
|
||||
return SetTangemPayCardLimitUseCase(cardDetailsRepository, paymentAccountStatusFetcher)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetTangemPayCryptoCurrencyStatusUseCase(
|
||||
paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
||||
): GetPaymentAccountCryptoCurrencyStatusUseCase {
|
||||
return GetPaymentAccountCryptoCurrencyStatusUseCase(paymentAccountStatusSupplier)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemPayMainScreenCustomerInfoUseCase(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.data.pay.entity
|
||||
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.domain.card.common.visa.VisaUtilities
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.requireUserWalletsSync
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class TangemPayCurrencyFactory @Inject constructor(
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val networkFactory: NetworkFactory,
|
||||
) {
|
||||
private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||
CryptoCurrencyFactory(excludedBlockchains)
|
||||
}
|
||||
|
||||
fun create(userWalletId: UserWalletId): CryptoCurrency.Token {
|
||||
val userWallet = userWalletsListRepository.requireUserWalletsSync()
|
||||
.firstOrNull { it.walletId == userWalletId }
|
||||
?: error("User wallet with id $userWalletId not found")
|
||||
val network = networkFactory.create(
|
||||
blockchain = VisaUtilities.visaBlockchain,
|
||||
userWallet = userWallet,
|
||||
extraDerivationPath = null,
|
||||
)
|
||||
return cryptoCurrencyFactory.createToken(
|
||||
network = requireNotNull(network),
|
||||
rawId = CryptoCurrency.RawID(TOKEN_ID),
|
||||
name = TOKEN_NAME,
|
||||
symbol = TOKEN_NAME,
|
||||
contractAddress = TOKEN_CONTRACT_ADDRESS,
|
||||
decimals = TOKEN_DECIMALS,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal const val TOKEN_ID = "usd-coin"
|
||||
internal const val TOKEN_NAME = "USDC"
|
||||
internal const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
|
||||
internal const val TOKEN_DECIMALS = 6
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.data.pay.flow
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
|
||||
import com.tangem.data.pay.store.PaymentAccountStatusesStore
|
||||
import com.tangem.domain.core.utils.catchOn
|
||||
import com.tangem.domain.models.StatusSource
|
||||
|
|
@ -8,8 +9,10 @@ import com.tangem.domain.models.account.Account
|
|||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.kyc.KycStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.pay.TangemPayCard
|
||||
import com.tangem.domain.models.pay.TangemPayCardLimitData
|
||||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
import com.tangem.domain.pay.model.OrderData
|
||||
|
|
@ -38,6 +41,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
private val customerOrderRepository: CustomerOrderRepository,
|
||||
private val deviceSecurity: DeviceSecurityInfoProvider,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory,
|
||||
private val eligibilityManager: TangemPayEligibilityManager,
|
||||
) : PaymentAccountStatusFetcher {
|
||||
|
||||
|
|
@ -142,7 +146,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
},
|
||||
ifRight = { customerInfo ->
|
||||
logger.i("proceedWithoutOrder data customerInfo ${account.userWalletId}")
|
||||
val status = customerInfo.mapToPaymentAccountStatus()
|
||||
val status = customerInfo.mapToPaymentAccountStatus(account.userWalletId)
|
||||
if (status is PaymentAccountStatusValue.IssuingCard && customerInfo.kycStatus == KycStatus.APPROVED) {
|
||||
// If order id wasn't saved -> start order creation and get customer info
|
||||
onboardingRepository.createOrder(account.userWalletId)
|
||||
|
|
@ -169,7 +173,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
KycStatus.PENDING,
|
||||
KycStatus.INIT,
|
||||
KycStatus.REJECTED,
|
||||
-> return customerInfo.mapToPaymentAccountStatus()
|
||||
-> return customerInfo.mapToPaymentAccountStatus(account.userWalletId)
|
||||
KycStatus.APPROVED -> Unit // proceed to order check
|
||||
}
|
||||
|
||||
|
|
@ -247,11 +251,11 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
return onboardingRepository.getCustomerInfo(userWalletId = account.userWalletId)
|
||||
.fold(
|
||||
ifLeft = { it.mapToPaymentAccountStatus(account.userWalletId) },
|
||||
ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus() },
|
||||
ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus(account.userWalletId) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun CustomerInfo.mapToPaymentAccountStatus(): PaymentAccountStatusValue {
|
||||
private fun CustomerInfo.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue {
|
||||
val cardInfo = this.cardInfo
|
||||
val productInstance = this.productInstance
|
||||
return if (kycStatus != KycStatus.APPROVED && !customerId.isNullOrEmpty()) {
|
||||
|
|
@ -262,6 +266,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
)
|
||||
} else if (cardInfo != null && productInstance != null && !customerId.isNullOrEmpty()) {
|
||||
convertToContentState(
|
||||
userWalletId = userWalletId,
|
||||
productInstance = productInstance,
|
||||
cardInfo = cardInfo,
|
||||
customerId = requireNotNull(customerId) { "CustomerId must not be null" },
|
||||
|
|
@ -272,34 +277,34 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
}
|
||||
|
||||
private fun convertToContentState(
|
||||
userWalletId: UserWalletId,
|
||||
productInstance: CustomerInfo.ProductInstance,
|
||||
cardInfo: CustomerInfo.CardInfo,
|
||||
customerId: String,
|
||||
): PaymentAccountStatusValue {
|
||||
return when (productInstance.frozenState) {
|
||||
TangemPayCardFrozenState.Frozen -> PaymentAccountStatusValue.Locked(
|
||||
source = StatusSource.ACTUAL,
|
||||
customerId = customerId,
|
||||
cardId = productInstance.cardId,
|
||||
lastFourDigits = cardInfo.lastFourDigits,
|
||||
currencyCode = cardInfo.currencyCode,
|
||||
depositAddress = cardInfo.depositAddress,
|
||||
isPinSet = cardInfo.isPinSet,
|
||||
fiatBalance = cardInfo.fiatBalance,
|
||||
cryptoBalance = cardInfo.cryptoBalance,
|
||||
)
|
||||
else -> PaymentAccountStatusValue.Loaded(
|
||||
source = StatusSource.ACTUAL,
|
||||
customerId = customerId,
|
||||
cardId = productInstance.cardId,
|
||||
lastFourDigits = cardInfo.lastFourDigits,
|
||||
currencyCode = cardInfo.currencyCode,
|
||||
depositAddress = cardInfo.depositAddress,
|
||||
isPinSet = cardInfo.isPinSet,
|
||||
fiatBalance = cardInfo.fiatBalance,
|
||||
cryptoBalance = cardInfo.cryptoBalance,
|
||||
)
|
||||
}
|
||||
val cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId)
|
||||
return PaymentAccountStatusValue.Loaded(
|
||||
source = StatusSource.ACTUAL,
|
||||
customerId = customerId,
|
||||
currencyCode = cardInfo.currencyCode,
|
||||
depositAddress = cardInfo.depositAddress,
|
||||
fiatBalance = cardInfo.fiatBalance,
|
||||
cryptoBalance = cardInfo.cryptoBalance,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
cards = listOf(
|
||||
TangemPayCard(
|
||||
id = productInstance.cardId,
|
||||
hasPinCode = cardInfo.isPinSet,
|
||||
displayName = productInstance.displayName,
|
||||
limit = TangemPayCardLimitData(
|
||||
actualCardLimit = productInstance.actualCardLimit,
|
||||
adminCardLimit = productInstance.adminCardLimit,
|
||||
),
|
||||
isFrozen = productInstance.frozenState is TangemPayCardFrozenState.Frozen,
|
||||
lastDigits = cardInfo.lastFourDigits,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun VisaApiError.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.data.pay.repository
|
|||
|
||||
import arrow.core.Either
|
||||
import arrow.core.flatMap
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
|
|
@ -15,7 +16,8 @@ import com.tangem.datasource.api.pay.models.response.OrderResponse
|
|||
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.TangemPayEligibilityType
|
||||
import com.tangem.domain.models.pay.TangemPayEligibilityType
|
||||
import com.tangem.domain.models.account.CardDisplayName
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
|
|
@ -26,6 +28,8 @@ import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
|||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
|
||||
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
|
||||
import com.tangem.domain.models.pay.TangemPayCardLimit
|
||||
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
|
|
@ -202,7 +206,16 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
}
|
||||
cardFrozenStateStore.store(key = instance.cardId, value = cardFrozenState)
|
||||
|
||||
ProductInstance(id = instance.id, cardId = instance.cardId, frozenState = cardFrozenState)
|
||||
val displayName = instance.displayName?.ifEmpty { null }
|
||||
|
||||
ProductInstance(
|
||||
id = instance.id,
|
||||
cardId = instance.cardId,
|
||||
frozenState = cardFrozenState,
|
||||
displayName = if (displayName != null) CardDisplayName(displayName).getOrElse { null } else null,
|
||||
actualCardLimit = instance.actualCardLimit?.parseCardLimit(),
|
||||
adminCardLimit = instance.adminCardLimit?.parseCardLimit(),
|
||||
)
|
||||
}
|
||||
return CustomerInfo(
|
||||
customerId = response?.id,
|
||||
|
|
@ -214,6 +227,13 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun CustomerMeResponse.CardLimit.parseCardLimit(): TangemPayCardLimit {
|
||||
return TangemPayCardLimit(
|
||||
amount = amount,
|
||||
period = TangemPayCardLimitPeriod.fromString(periodType),
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendKycAnalytics(kycStatus: KycStatus) {
|
||||
val event = when (kycStatus) {
|
||||
KycStatus.APPROVED -> TangemPayAnalyticsEvents.KycPassedAndOrderCreated()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.right
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.ReissueCardRequest
|
||||
import com.tangem.datasource.api.pay.models.response.OrderResponse
|
||||
import com.tangem.datasource.local.visa.TangemPayReissueCardStore
|
||||
import com.tangem.domain.models.pay.TangemPayReissueCardFee
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.model.OrderStatus
|
||||
import com.tangem.domain.pay.model.TangemPayReissueOrderInfo
|
||||
import com.tangem.domain.pay.repository.TangemPayReissueCardRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultReissueCardRepository @Inject constructor(
|
||||
private val tangemPayApi: TangemPayApi,
|
||||
private val requestHelper: TangemPayRequestPerformer,
|
||||
private val tangemPayReissueCardStore: TangemPayReissueCardStore,
|
||||
) : TangemPayReissueCardRepository {
|
||||
|
||||
override suspend fun getReissueCardFee(userWalletId: UserWalletId): Either<VisaApiError, TangemPayReissueCardFee> =
|
||||
either {
|
||||
runSuspendCatching {
|
||||
tangemPayReissueCardStore.getReissueFee(userWalletId)?.let { return Either.Right(it) }
|
||||
}
|
||||
|
||||
val response = requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.getFee(
|
||||
authHeader = authHeader,
|
||||
type = CARD_REPLACEMENT_FEE_TYPE,
|
||||
)
|
||||
}.bind()
|
||||
|
||||
val result = response.result
|
||||
val fee = TangemPayReissueCardFee(
|
||||
amount = result.amount.toBigDecimal(),
|
||||
currencyCode = result.currency,
|
||||
)
|
||||
|
||||
runSuspendCatching {
|
||||
tangemPayReissueCardStore.storeReissueFee(userWalletId, fee)
|
||||
}
|
||||
|
||||
fee
|
||||
}
|
||||
|
||||
override suspend fun reissueCard(
|
||||
userWalletId: UserWalletId,
|
||||
cardId: String,
|
||||
): Either<VisaApiError, TangemPayReissueOrderInfo> = either {
|
||||
val response = requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.reissueCard(
|
||||
authHeader = authHeader,
|
||||
body = ReissueCardRequest(cardId = cardId),
|
||||
)
|
||||
}.bind()
|
||||
|
||||
TangemPayReissueOrderInfo(response.result.orderId, OrderStatus.fromString(response.result.status))
|
||||
}
|
||||
|
||||
override suspend fun storeReissueOrderId(cardId: String, orderId: String): Either<UniversalError, Unit> =
|
||||
runSuspendCatching {
|
||||
tangemPayReissueCardStore.storeReissueOrderId(cardId, orderId)
|
||||
}.fold(
|
||||
onSuccess = { Unit.right() },
|
||||
onFailure = { Either.Left(VisaApiError.Unspecified) },
|
||||
)
|
||||
|
||||
override suspend fun getReissueOrderInfo(
|
||||
userWalletId: UserWalletId,
|
||||
cardId: String,
|
||||
): Either<UniversalError, TangemPayReissueOrderInfo?> = either {
|
||||
val orderId = runSuspendCatching { tangemPayReissueCardStore.getOrderId(cardId) }.getOrNull()
|
||||
|
||||
if (orderId == null) {
|
||||
return null.right()
|
||||
}
|
||||
|
||||
val order = requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.getOrder(authHeader, orderId)
|
||||
}.bind()
|
||||
|
||||
val result = order.result ?: raise(VisaApiError.Unspecified)
|
||||
|
||||
TangemPayReissueOrderInfo(
|
||||
orderId = result.id,
|
||||
orderStatus = when (result.status) {
|
||||
OrderResponse.Result.Status.NEW -> OrderStatus.NEW
|
||||
OrderResponse.Result.Status.PROCESSING -> OrderStatus.PROCESSING
|
||||
OrderResponse.Result.Status.COMPLETED -> OrderStatus.COMPLETED
|
||||
OrderResponse.Result.Status.CANCELED -> OrderStatus.CANCELED
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val CARD_REPLACEMENT_FEE_TYPE = "CARD_REPLACEMENT"
|
||||
}
|
||||
}
|
||||
|
|
@ -14,11 +14,13 @@ import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
|||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.CardDetailsRequest
|
||||
import com.tangem.datasource.api.pay.models.request.FreezeUnfreezeCardRequest
|
||||
import com.tangem.datasource.api.pay.models.request.UpdateCardRequest
|
||||
import com.tangem.datasource.api.pay.models.request.SetPinRequest
|
||||
import com.tangem.datasource.api.pay.models.response.FreezeUnfreezeCardResponse
|
||||
import com.tangem.datasource.api.pay.models.response.OrderResponse.Result.Status
|
||||
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.domain.models.account.CardDisplayName
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.model.SetPinResult
|
||||
import com.tangem.domain.pay.model.TangemPayCardBalance
|
||||
|
|
@ -39,7 +41,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||
|
||||
private const val TAG = "TangemPay: CardDetailsRepository"
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
|
||||
private val tangemPayApi: TangemPayApi,
|
||||
private val requestHelper: TangemPayRequestPerformer,
|
||||
|
|
@ -318,6 +320,54 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun updateCardDisplayName(
|
||||
cardId: String,
|
||||
userWalletId: UserWalletId,
|
||||
displayName: CardDisplayName,
|
||||
): Either<UniversalError, Unit> {
|
||||
return catch(
|
||||
block = {
|
||||
requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.updateCard(
|
||||
authHeader = authHeader,
|
||||
body = UpdateCardRequest(
|
||||
displayName = displayName.value,
|
||||
),
|
||||
cardId = cardId,
|
||||
)
|
||||
}.fold(
|
||||
ifLeft = { error -> error.left() },
|
||||
ifRight = { Unit.right() },
|
||||
)
|
||||
},
|
||||
catch = ::catchException,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun updateCardLimit(
|
||||
cardId: String,
|
||||
userWalletId: UserWalletId,
|
||||
limit: String,
|
||||
): Either<UniversalError, Unit> {
|
||||
return catch(
|
||||
block = {
|
||||
requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.updateCard(
|
||||
authHeader = authHeader,
|
||||
body = UpdateCardRequest(
|
||||
cardLimit = UpdateCardRequest.CardLimit(limit),
|
||||
),
|
||||
cardId = cardId,
|
||||
)
|
||||
}.fold(
|
||||
ifLeft = { error -> error.left() },
|
||||
ifRight = { Unit.right() },
|
||||
)
|
||||
},
|
||||
catch = ::catchException,
|
||||
)
|
||||
}
|
||||
|
||||
override fun cardFrozenState(cardId: String): Flow<TangemPayCardFrozenState> {
|
||||
return cardFrozenStateStore.get(cardId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.tangem.domain.models.account.AccountStatus
|
|||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -29,6 +30,7 @@ internal typealias WalletIdWithPaymentStatusDM = Map<String, PaymentAccountStatu
|
|||
internal class PaymentAccountStatusesStore(
|
||||
private val runtimeStore: RuntimeSharedStore<WalletIdWithPaymentStatus>,
|
||||
private val persistenceDataStore: DataStore<WalletIdWithPaymentStatusDM>,
|
||||
private val converter: PaymentAccountStatusValueDMConverter,
|
||||
scope: AppCoroutineScope,
|
||||
) {
|
||||
|
||||
|
|
@ -39,11 +41,12 @@ internal class PaymentAccountStatusesStore(
|
|||
runtimeStore.store(
|
||||
value = cachedStatuses.mapValues { (rawUserWalletId, statusDM) ->
|
||||
val account = Account.Payment(userWalletId = UserWalletId(rawUserWalletId))
|
||||
val statusValue = PaymentAccountStatusValueDMConverter.convertBack(value = statusDM)
|
||||
val statusValue = converter.convertBack(userWalletId = account.userWalletId, value = statusDM)
|
||||
AccountStatus.Payment(account = account, value = statusValue)
|
||||
},
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
runSuspendCatching { persistenceDataStore.updateData { emptyMap() } }
|
||||
TangemLogger.e("Error while loading cached payment account statuses", e)
|
||||
}
|
||||
}
|
||||
|
|
@ -87,7 +90,7 @@ internal class PaymentAccountStatusesStore(
|
|||
}
|
||||
|
||||
private suspend fun storeInPersistence(userWalletId: UserWalletId, status: PaymentAccountStatusValue) {
|
||||
val statusDM = PaymentAccountStatusValueDMConverter.convert(value = status) ?: return
|
||||
val statusDM = converter.convert(value = status) ?: return
|
||||
persistenceDataStore.updateData { storedStatuses ->
|
||||
storedStatuses.toMutableMap().apply {
|
||||
put(key = userWalletId.stringValue, value = statusDM)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ dependencies {
|
|||
/* Project - Core */
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.analytics)
|
||||
api(projects.core.configToggles)
|
||||
|
||||
/* DI */
|
||||
implementation(deps.hilt.core)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
|||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.data.walletconnect.DefaultWalletConnectRepository
|
||||
import com.tangem.data.walletconnect.initialize.DefaultWcInitializeUseCase
|
||||
import com.tangem.data.walletconnect.network.bitcoin.WcBitcoinNetwork
|
||||
import com.tangem.data.walletconnect.network.ethereum.WcEthNetwork
|
||||
import com.tangem.data.walletconnect.network.solana.WcSolanaNetwork
|
||||
import com.tangem.data.walletconnect.pair.*
|
||||
|
|
@ -25,6 +26,7 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository
|
|||
import com.tangem.domain.walletconnect.WcPairService
|
||||
import com.tangem.domain.walletconnect.WcRequestService
|
||||
import com.tangem.domain.walletconnect.WcRequestUseCaseFactory
|
||||
import com.tangem.domain.walletconnect.featuretoggle.WalletConnectFeatureToggles
|
||||
import com.tangem.domain.walletconnect.repository.WalletConnectRepository
|
||||
import com.tangem.domain.walletconnect.repository.WcSessionsManager
|
||||
import com.tangem.domain.walletconnect.usecase.disconnect.WcDisconnectUseCase
|
||||
|
|
@ -34,6 +36,8 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
|
|||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.data.walletconnect.featuretoggle.DefaultWalletConnectFeatureToggles
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -162,6 +166,22 @@ internal object WalletConnectDataModule {
|
|||
networksConverter = wcNetworksConverter,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun wcBitcoinNetwork(
|
||||
@SdkMoshi moshi: Moshi,
|
||||
wcNetworksConverter: WcNetworksConverter,
|
||||
sessionsManager: WcSessionsManager,
|
||||
factories: WcBitcoinNetwork.Factories,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
): WcBitcoinNetwork = WcBitcoinNetwork(
|
||||
moshi = moshi,
|
||||
sessionsManager = sessionsManager,
|
||||
factories = factories,
|
||||
networksConverter = wcNetworksConverter,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun caipNamespaceDelegate(
|
||||
|
|
@ -198,11 +218,19 @@ internal object WalletConnectDataModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun diHelperBox(ethNetwork: WcEthNetwork, solanaNetwork: WcSolanaNetwork) = DiHelperBox(
|
||||
handlers = setOf(
|
||||
ethNetwork,
|
||||
solanaNetwork,
|
||||
),
|
||||
fun diHelperBox(
|
||||
ethNetwork: WcEthNetwork,
|
||||
solanaNetwork: WcSolanaNetwork,
|
||||
bitcoinNetwork: WcBitcoinNetwork,
|
||||
featureToggles: WalletConnectFeatureToggles,
|
||||
) = DiHelperBox(
|
||||
handlers = buildSet {
|
||||
add(ethNetwork)
|
||||
add(solanaNetwork)
|
||||
if (featureToggles.isBitcoinEnabled) {
|
||||
add(bitcoinNetwork)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
|
@ -210,10 +238,15 @@ internal object WalletConnectDataModule {
|
|||
fun namespaceConverters(
|
||||
ethNamespaceConverter: WcEthNetwork.NamespaceConverter,
|
||||
solanaNamespaceConverter: WcSolanaNetwork.NamespaceConverter,
|
||||
): Set<@JvmSuppressWildcards WcNamespaceConverter> = setOf(
|
||||
ethNamespaceConverter,
|
||||
solanaNamespaceConverter,
|
||||
)
|
||||
bitcoinNamespaceConverter: WcBitcoinNetwork.NamespaceConverter,
|
||||
featureToggles: WalletConnectFeatureToggles,
|
||||
): Set<@JvmSuppressWildcards WcNamespaceConverter> = buildSet {
|
||||
add(ethNamespaceConverter)
|
||||
add(solanaNamespaceConverter)
|
||||
if (featureToggles.isBitcoinEnabled) {
|
||||
add(bitcoinNamespaceConverter)
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
|
@ -239,6 +272,14 @@ internal object WalletConnectDataModule {
|
|||
return WcSolanaNetwork.NamespaceConverter(excludedBlockchains)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun wcBitcoinNetworkNamespaceConverter(
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
): WcBitcoinNetwork.NamespaceConverter {
|
||||
return WcBitcoinNetwork.NamespaceConverter(excludedBlockchains)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesWcDisconnectUseCase(
|
||||
|
|
@ -248,6 +289,14 @@ internal object WalletConnectDataModule {
|
|||
return WcDisconnectUseCase(sessionsManager, analytics)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesWalletConnectFeatureToggles(
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
): WalletConnectFeatureToggles {
|
||||
return DefaultWalletConnectFeatureToggles(featureTogglesManager)
|
||||
}
|
||||
|
||||
internal class DiHelperBox(
|
||||
val handlers: Set<WcRequestToUseCaseConverter>,
|
||||
)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue