Updated on 2026-08-14
This commit is contained in:
commit
226a413558
994 changed files with 27178 additions and 11929 deletions
|
|
@ -37,7 +37,9 @@ dependencies {
|
|||
|
||||
implementation(projects.domain.feedback)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
}
|
||||
|
|
@ -9,12 +9,14 @@ import com.tangem.data.feedback.converters.CardInfoConverter
|
|||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMap
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
||||
import com.tangem.domain.feedback.models.*
|
||||
import com.tangem.domain.feedback.repository.FeedbackRepository
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runCatching
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
|
|
@ -25,43 +27,47 @@ import java.io.StringWriter
|
|||
/**
|
||||
* Implementation of [FeedbackRepository]
|
||||
*
|
||||
* @property appPreferencesStore application preferences store
|
||||
* @property userWalletsStore user wallets store
|
||||
* @property walletManagersStore wallet managers store
|
||||
* @property context context for getting app version
|
||||
* @property appPreferencesStore application preferences store
|
||||
* @property userWalletsListManager user wallets list manager
|
||||
* @property walletManagersStore wallet managers store
|
||||
* @property context context for getting app version
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultFeedbackRepository(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val walletManagersStore: WalletManagersStore,
|
||||
private val context: Context,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : FeedbackRepository {
|
||||
|
||||
private val blockchainsErrors = MutableStateFlow<Map<UserWalletId, BlockchainErrorInfo>>(emptyMap())
|
||||
|
||||
override suspend fun getUserWalletsInfo(): UserWalletsInfo {
|
||||
override suspend fun getCardInfo(scanResponse: ScanResponse) = CardInfoConverter.convert(value = scanResponse)
|
||||
|
||||
override suspend fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo {
|
||||
return UserWalletsInfo(
|
||||
selectedUserWalletId = getSelectedUserWallet().walletId.stringValue,
|
||||
totalUserWallets = userWalletsStore.getAllSyncOrNull()?.size ?: error("No user wallets found"),
|
||||
selectedUserWalletId = userWalletId?.stringValue ?: "card isn't activated",
|
||||
totalUserWallets = userWalletsListManager.walletsCount,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getCardInfo(): CardInfo {
|
||||
return CardInfoConverter.convert(value = getSelectedUserWallet())
|
||||
}
|
||||
|
||||
override suspend fun getBlockchainInfoList(): List<BlockchainInfo> {
|
||||
override suspend fun getBlockchainInfoList(userWalletId: UserWalletId): List<BlockchainInfo> {
|
||||
return walletManagersStore
|
||||
.getAllSync(userWalletId = getSelectedUserWallet().walletId)
|
||||
.getAllSync(userWalletId = userWalletId)
|
||||
.map(BlockchainInfoConverter::convert)
|
||||
}
|
||||
|
||||
override suspend fun getBlockchainInfo(blockchainId: String, derivationPath: String?): BlockchainInfo? {
|
||||
override suspend fun getBlockchainInfo(
|
||||
userWalletId: UserWalletId,
|
||||
blockchainId: String,
|
||||
derivationPath: String?,
|
||||
): BlockchainInfo? {
|
||||
return walletManagersStore
|
||||
.getSyncOrNull(
|
||||
userWalletId = getSelectedUserWallet().walletId,
|
||||
userWalletId = userWalletId,
|
||||
blockchain = Blockchain.fromId(blockchainId),
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
|
|
@ -77,18 +83,18 @@ internal class DefaultFeedbackRepository(
|
|||
}
|
||||
|
||||
override fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) {
|
||||
val userWallet = userWalletsListManager.selectedUserWalletSync ?: error("UserWallet is not selected")
|
||||
|
||||
blockchainsErrors.update {
|
||||
it.toMutableMap().apply {
|
||||
put(getSelectedUserWallet().walletId, error)
|
||||
put(userWallet.walletId, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getBlockchainErrorInfo(): BlockchainErrorInfo? {
|
||||
return blockchainsErrors.value[getSelectedUserWallet().walletId].also {
|
||||
if (it == null) {
|
||||
Timber.e("Blockchain error info is null for ${getSelectedUserWallet().walletId}")
|
||||
}
|
||||
override suspend fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo? {
|
||||
return blockchainsErrors.value[userWalletId].also {
|
||||
if (it == null) Timber.e("Blockchain error info is null for $userWalletId")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,7 +105,7 @@ internal class DefaultFeedbackRepository(
|
|||
}
|
||||
|
||||
override suspend fun createLogFile(logs: String): File? {
|
||||
return try {
|
||||
return runCatching(dispatchers.io) {
|
||||
val file = File(context.filesDir, LOGS_FILE)
|
||||
file.delete()
|
||||
file.createNewFile()
|
||||
|
|
@ -113,8 +119,8 @@ internal class DefaultFeedbackRepository(
|
|||
fileWriter.close()
|
||||
|
||||
file
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Logs file isn't created")
|
||||
}.getOrElse {
|
||||
Timber.e(it, "Logs file isn't created")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -130,11 +136,6 @@ internal class DefaultFeedbackRepository(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getSelectedUserWallet(): UserWallet {
|
||||
return userWalletsStore.selectedUserWalletOrNull
|
||||
?: error("UserWallet is not selected")
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val LOGS_FILE = "logs.txt"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,28 +2,36 @@ package com.tangem.data.feedback.converters
|
|||
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.feedback.models.CardInfo
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter from [UserWallet] to [CardInfo]
|
||||
* Converter from [ScanResponse] to [CardInfo]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object CardInfoConverter : Converter<UserWallet, CardInfo> {
|
||||
internal object CardInfoConverter : Converter<ScanResponse, CardInfo> {
|
||||
|
||||
override fun convert(value: UserWallet): CardInfo {
|
||||
return with(value.scanResponse) {
|
||||
override fun convert(value: ScanResponse): CardInfo {
|
||||
return with(value) {
|
||||
CardInfo(
|
||||
userWalletId = createUserWalletId(scanResponse = value),
|
||||
cardId = card.cardId,
|
||||
firmwareVersion = card.firmwareVersion.stringValue,
|
||||
cardBlockchain = walletData?.blockchain,
|
||||
signedHashesList = card.wallets.map {
|
||||
CardInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString())
|
||||
},
|
||||
isImported = value.isImported,
|
||||
isStart2Coin = value.scanResponse.card.isStart2Coin,
|
||||
isImported = value.card.wallets.any(CardDTO.Wallet::isImported),
|
||||
isStart2Coin = value.card.isStart2Coin,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createUserWalletId(scanResponse: ScanResponse): UserWalletId? {
|
||||
return UserWalletIdBuilder.scanResponse(scanResponse = scanResponse).build()
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,10 @@ package com.tangem.data.feedback.di
|
|||
import android.content.Context
|
||||
import com.tangem.data.feedback.DefaultFeedbackRepository
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
||||
import com.tangem.domain.feedback.repository.FeedbackRepository
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -21,10 +22,17 @@ internal object FeedbackRepositoryModule {
|
|||
@Singleton
|
||||
fun provideFeedbackRepository(
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
userWalletsListManager: UserWalletsListManager,
|
||||
walletManagersStore: WalletManagersStore,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): FeedbackRepository {
|
||||
return DefaultFeedbackRepository(appPreferencesStore, userWalletsStore, walletManagersStore, context)
|
||||
return DefaultFeedbackRepository(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
userWalletsListManager = userWalletsListManager,
|
||||
walletManagersStore = walletManagersStore,
|
||||
context = context,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
1
data/markets/.gitignore
vendored
Normal file
1
data/markets/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
34
data/markets/build.gradle.kts
Normal file
34
data/markets/build.gradle.kts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.data.markets"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.pagination)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.markets)
|
||||
implementation(projects.data.common)
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
// region Others dependencies
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package com.tangem.data.markets
|
||||
|
||||
import com.tangem.data.markets.converters.*
|
||||
import com.tangem.data.markets.utils.retryOnError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.markets.TangemTechMarketsApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.domain.markets.*
|
||||
import com.tangem.domain.markets.repositories.MarketsTokenRepository
|
||||
import com.tangem.pagination.*
|
||||
import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
internal class DefaultMarketsTokenRepository(
|
||||
private val marketsApi: TangemTechMarketsApi,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
||||
) : MarketsTokenRepository {
|
||||
|
||||
private val tokenListConverter = TokenMarketListConverter()
|
||||
|
||||
private val tokenMarketsFetcher
|
||||
get() = LimitOffsetBatchFetcher(
|
||||
prefetchDistance = 150,
|
||||
batchSize = 100,
|
||||
subFetcher = object : LimitOffsetBatchFetcher.SubFetcher<TokenMarketListConfig, List<TokenMarket>> {
|
||||
|
||||
var requestTimeStamp: Long? = null // TODO when backend is ready
|
||||
|
||||
override suspend fun fetch(
|
||||
request: LimitOffsetBatchFetcher.Request<TokenMarketListConfig>,
|
||||
lastResult: BatchFetchResult<List<TokenMarket>>?,
|
||||
): BatchFetchResult<List<TokenMarket>> {
|
||||
val res = retryOnError(priority = true) {
|
||||
marketsApi.getCoinsList(
|
||||
currency = request.params.fiatPriceCurrency,
|
||||
interval = request.params.priceChangeInterval.toRequestParam(),
|
||||
order = request.params.order.toRequestParam(),
|
||||
search = request.params.searchText,
|
||||
generalCoins = request.params.showUnder100kMarketCapTokens.not(),
|
||||
offset = request.offset,
|
||||
limit = request.limit,
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
val last = res.tokens.size < request.limit
|
||||
|
||||
return BatchFetchResult.Success(
|
||||
data = tokenListConverter.convert(res),
|
||||
last = last,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
override fun getTokenListFlow(
|
||||
batchingContext: BatchingContext<Int, TokenMarketListConfig, TokenMarketUpdateRequest>,
|
||||
): BatchFlow<Int, List<TokenMarket>, TokenMarketUpdateRequest> {
|
||||
val tokenMarketsUpdateFetcher = MarketsBatchUpdateFetcher(
|
||||
tangemTechApi = tangemTechApi,
|
||||
marketsApi = marketsApi,
|
||||
)
|
||||
|
||||
return BatchListSource(
|
||||
fetchDispatcher = dispatcherProvider.io,
|
||||
context = batchingContext,
|
||||
generateNewKey = { it.size },
|
||||
batchFetcher = tokenMarketsFetcher,
|
||||
updateFetcher = tokenMarketsUpdateFetcher,
|
||||
).toBatchFlow()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package com.tangem.data.markets
|
||||
|
||||
import com.tangem.data.markets.converters.TokenListChartConverter
|
||||
import com.tangem.data.markets.converters.TokenMarketChartsConverter
|
||||
import com.tangem.data.markets.converters.TokenQuotesConverter
|
||||
import com.tangem.data.markets.converters.toRequestParam
|
||||
import com.tangem.data.markets.utils.retryOnError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.markets.TangemTechMarketsApi
|
||||
import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.domain.markets.TokenMarketUpdateRequest
|
||||
import com.tangem.pagination.Batch
|
||||
import com.tangem.pagination.BatchUpdateFetcher
|
||||
import com.tangem.pagination.BatchUpdateResult
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
internal class MarketsBatchUpdateFetcher(
|
||||
private val marketsApi: TangemTechMarketsApi,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
) : BatchUpdateFetcher<Int, List<TokenMarket>, TokenMarketUpdateRequest> {
|
||||
|
||||
private val tokenListChartsConverter = TokenMarketChartsConverter(TokenListChartConverter())
|
||||
private val tokenQuotesConverter = TokenQuotesConverter()
|
||||
|
||||
override suspend fun BatchUpdateFetcher.UpdateContext<Int, List<TokenMarket>>.fetchUpdateAsync(
|
||||
toUpdate: List<Batch<Int, List<TokenMarket>>>,
|
||||
updateRequest: TokenMarketUpdateRequest,
|
||||
) {
|
||||
val idsToUpdate = toUpdate.map { batch ->
|
||||
batch.key to batch.data.map { it.id }
|
||||
}
|
||||
|
||||
when (updateRequest) {
|
||||
is TokenMarketUpdateRequest.UpdateChart -> coroutineScope {
|
||||
val updateTasks = idsToUpdate.map { batchIds ->
|
||||
async {
|
||||
retryOnError {
|
||||
marketsApi.getCoinsListCharts(
|
||||
coinIds = batchIds.second.joinToString(separator = ","),
|
||||
interval = updateRequest.interval.toRequestParam(),
|
||||
currency = updateRequest.currency,
|
||||
).getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateTasks.forEachIndexed { index, deferred ->
|
||||
launch {
|
||||
val res = deferred.await()
|
||||
val batchToUpdate = toUpdate[index]
|
||||
|
||||
update {
|
||||
val resBatch = changeChartsInBatches(
|
||||
updateRequest = updateRequest,
|
||||
batchToUpdate = batchToUpdate,
|
||||
update = res,
|
||||
)
|
||||
BatchUpdateResult.Success(resBatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is TokenMarketUpdateRequest.UpdateQuotes -> {
|
||||
val quotesRes = retryOnError {
|
||||
tangemTechApi.getQuotes(
|
||||
currencyId = updateRequest.currencyId,
|
||||
coinIds = idsToUpdate.map { it.second }.flatten().joinToString(separator = ","),
|
||||
fields = quoteFields.joinToString(separator = ","),
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
update {
|
||||
val res = toUpdate.map { batch ->
|
||||
batch.copy(
|
||||
data = batch.data.map {
|
||||
it.copy(tokenQuotes = tokenQuotesConverter.convert(it.id, quotesRes))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
BatchUpdateResult.Success(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Batch<Int, List<TokenMarket>>>.changeChartsInBatches(
|
||||
updateRequest: TokenMarketUpdateRequest.UpdateChart,
|
||||
batchToUpdate: Batch<Int, List<TokenMarket>>,
|
||||
update: TokenMarketChartListResponse,
|
||||
): List<Batch<Int, List<TokenMarket>>> = mapNotNull { resultBatch ->
|
||||
if (batchToUpdate.key != resultBatch.key) return@mapNotNull null
|
||||
|
||||
Batch(
|
||||
key = batchToUpdate.key,
|
||||
data = batchToUpdate.data.map {
|
||||
it.copy(
|
||||
tokenCharts = tokenListChartsConverter.convert(
|
||||
chartsToCopy = it.tokenCharts,
|
||||
tokenId = it.id,
|
||||
interval = updateRequest.interval,
|
||||
value = update,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val quoteFields = listOf(
|
||||
"price",
|
||||
"priceChange24h",
|
||||
"priceChange1w",
|
||||
"priceChange30d",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.data.markets.converters
|
||||
|
||||
import com.tangem.datasource.api.markets.models.response.TokenMarketChartResponse
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenChart
|
||||
|
||||
class TokenListChartConverter {
|
||||
|
||||
fun convert(interval: PriceChangeInterval, value: TokenMarketChartResponse): TokenChart {
|
||||
return TokenChart(
|
||||
interval = interval,
|
||||
priceY = value.prices.values.toList(),
|
||||
timeStamp = value.prices.keys.toList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.data.markets.converters
|
||||
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketListConfig
|
||||
|
||||
fun TokenMarketListConfig.Interval.toRequestParam(): String = when (this) {
|
||||
TokenMarketListConfig.Interval.H24 -> "24h"
|
||||
TokenMarketListConfig.Interval.WEEK -> "1w"
|
||||
TokenMarketListConfig.Interval.MONTH -> "30d"
|
||||
}
|
||||
|
||||
fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) {
|
||||
TokenMarketListConfig.Order.ByRating -> "rating"
|
||||
TokenMarketListConfig.Order.Trending -> "trending"
|
||||
TokenMarketListConfig.Order.Buyers -> "buyers"
|
||||
TokenMarketListConfig.Order.TopGainers -> "gainers"
|
||||
TokenMarketListConfig.Order.TopLosers -> "losers"
|
||||
}
|
||||
|
||||
fun PriceChangeInterval.toRequestParam(): String = when (this) {
|
||||
PriceChangeInterval.H24 -> "24h"
|
||||
PriceChangeInterval.WEEK -> "1w"
|
||||
PriceChangeInterval.MONTH -> "30d"
|
||||
PriceChangeInterval.MONTH3 -> "3m"
|
||||
PriceChangeInterval.MONTH6 -> "6m"
|
||||
PriceChangeInterval.YEAR -> "1y"
|
||||
PriceChangeInterval.ALL_TIME -> "all"
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.data.markets.converters
|
||||
|
||||
import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
|
||||
class TokenMarketChartsConverter(
|
||||
private val tokenListChartConverter: TokenListChartConverter,
|
||||
) {
|
||||
|
||||
fun convert(
|
||||
chartsToCopy: TokenMarket.Charts,
|
||||
tokenId: String,
|
||||
interval: PriceChangeInterval,
|
||||
value: TokenMarketChartListResponse,
|
||||
): TokenMarket.Charts {
|
||||
val prices = requireNotNull(value[tokenId]) {
|
||||
"$tokenId is not found in the response. This shouldn't have happened."
|
||||
}
|
||||
return when (interval) {
|
||||
PriceChangeInterval.H24 -> chartsToCopy.copy(
|
||||
h24 = tokenListChartConverter.convert(interval, prices),
|
||||
)
|
||||
PriceChangeInterval.WEEK -> chartsToCopy.copy(
|
||||
week = tokenListChartConverter.convert(interval, prices),
|
||||
)
|
||||
PriceChangeInterval.MONTH -> chartsToCopy.copy(
|
||||
month = tokenListChartConverter.convert(interval, prices),
|
||||
)
|
||||
else -> error("unsupported interval=$interval. This shouldn't have happened.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.data.markets.converters
|
||||
|
||||
import com.tangem.datasource.api.markets.models.response.TokenMarketListResponse
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.domain.markets.TokenQuotes
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
class TokenMarketListConverter : Converter<TokenMarketListResponse, List<TokenMarket>> {
|
||||
|
||||
override fun convert(value: TokenMarketListResponse): List<TokenMarket> {
|
||||
return value.tokens.map { token ->
|
||||
TokenMarket(
|
||||
id = token.id,
|
||||
name = token.name,
|
||||
symbol = token.symbol,
|
||||
marketRating = token.marketRating,
|
||||
marketCap = token.marketCap,
|
||||
imageHost = value.imageHost,
|
||||
tokenQuotes = TokenQuotes(
|
||||
currentPrice = token.currentPrice,
|
||||
priceChanges = mapOf(
|
||||
PriceChangeInterval.H24 to token.priceChangePercentage.h24.movePointLeft(2),
|
||||
PriceChangeInterval.WEEK to token.priceChangePercentage.week1.movePointLeft(2),
|
||||
PriceChangeInterval.MONTH to token.priceChangePercentage.day30.movePointLeft(2),
|
||||
),
|
||||
),
|
||||
tokenCharts = TokenMarket.Charts(null, null, null),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.data.markets.converters
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenQuotes
|
||||
import java.math.BigDecimal
|
||||
|
||||
class TokenQuotesConverter {
|
||||
|
||||
fun convert(tokenId: String, value: QuotesResponse): TokenQuotes {
|
||||
val quote = requireNotNull(value.quotes[tokenId]) {
|
||||
"$tokenId is not found in the response. This shouldn't have happened."
|
||||
}
|
||||
return TokenQuotes(
|
||||
currentPrice = requireNotNull(quote.price) {
|
||||
"Price is not found in the QuotesResponse. This shouldn't have happened."
|
||||
},
|
||||
priceChanges = mapOf(
|
||||
PriceChangeInterval.H24 to (quote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2),
|
||||
PriceChangeInterval.WEEK to (quote.priceChange1w ?: BigDecimal.ZERO).movePointLeft(2),
|
||||
PriceChangeInterval.MONTH to (quote.priceChange30d ?: BigDecimal.ZERO).movePointLeft(2),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.data.markets.di
|
||||
|
||||
import com.tangem.data.markets.DefaultMarketsTokenRepository
|
||||
import com.tangem.datasource.api.markets.TangemTechMarketsApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.di.DevTangemApi
|
||||
import com.tangem.domain.markets.repositories.MarketsTokenRepository
|
||||
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 MarketsDataModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMarketsRepository(
|
||||
@DevTangemApi marketsApi: TangemTechMarketsApi,
|
||||
@DevTangemApi tangemTechApi: TangemTechApi,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): MarketsTokenRepository {
|
||||
return DefaultMarketsTokenRepository(
|
||||
marketsApi = marketsApi,
|
||||
tangemTechApi = tangemTechApi,
|
||||
dispatcherProvider = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.data.markets.utils
|
||||
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.yield
|
||||
import timber.log.Timber
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
@Suppress("UnconditionalJumpStatementInLoop")
|
||||
internal suspend fun <T> retryOnError(priority: Boolean = false, call: suspend () -> T): T {
|
||||
while (true) {
|
||||
return try {
|
||||
call()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) {
|
||||
currentCoroutineContext().ensureActive()
|
||||
}
|
||||
Timber.e(e)
|
||||
if (priority.not()) {
|
||||
yield()
|
||||
delay(timeMillis = 500)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package com.tangem.data.settings
|
||||
|
||||
import android.os.SystemClock
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.getIsFirstTimeAskingPermission
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.getPermissionDaysCount
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.getPermissionLaunchCount
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.getShouldShowInitialPermissionScreen
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.getShouldShowPermission
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||
import com.tangem.datasource.local.preferences.utils.store
|
||||
import com.tangem.domain.settings.repositories.PermissionRepository
|
||||
|
||||
internal class DefaultPermissionRepository(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : PermissionRepository {
|
||||
|
||||
override suspend fun shouldInitiallyShowPermissionScreen(permission: String): Boolean {
|
||||
val key = getShouldShowInitialPermissionScreen(permission)
|
||||
val initialPermissionScreen = appPreferencesStore.getSyncOrDefault(key = key, default = true)
|
||||
if (initialPermissionScreen) appPreferencesStore.store(key = key, value = false)
|
||||
return initialPermissionScreen
|
||||
}
|
||||
|
||||
override suspend fun isFirstTimeAskingPermission(permission: String): Boolean =
|
||||
appPreferencesStore.getSyncOrDefault(
|
||||
key = getIsFirstTimeAskingPermission(permission),
|
||||
default = true,
|
||||
)
|
||||
|
||||
override suspend fun setFirstTimeAskingPermission(permission: String, value: Boolean) {
|
||||
appPreferencesStore.store(
|
||||
key = getIsFirstTimeAskingPermission(permission),
|
||||
value = value,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun shouldAskPermission(permission: String): Boolean {
|
||||
val shouldAskPermission = appPreferencesStore.getSyncOrDefault(getShouldShowPermission(permission), true)
|
||||
val delayedLaunches = appPreferencesStore.getSyncOrDefault(getPermissionLaunchCount(permission), 0)
|
||||
val delayedDays = appPreferencesStore.getSyncOrDefault(getPermissionDaysCount(permission), 0)
|
||||
val currentLaunchCounter = appPreferencesStore.getSyncOrDefault(PreferencesKeys.APP_LAUNCH_COUNT_KEY, 0)
|
||||
|
||||
val nowMillis = SystemClock.elapsedRealtime()
|
||||
val isDaysDelayed = delayedDays < nowMillis
|
||||
val isLaunchesDelayed = delayedLaunches < currentLaunchCounter
|
||||
return shouldAskPermission && isDaysDelayed && isLaunchesDelayed
|
||||
}
|
||||
|
||||
override suspend fun neverAskPermission(permission: String) {
|
||||
appPreferencesStore.store(key = getShouldShowPermission(permission), value = false)
|
||||
}
|
||||
|
||||
override suspend fun delayPermissionAsking(permission: String) {
|
||||
appPreferencesStore.editData {
|
||||
val appLaunchCounter = it.getOrDefault(PreferencesKeys.APP_LAUNCH_COUNT_KEY, 0)
|
||||
val nowMillis = SystemClock.elapsedRealtime()
|
||||
|
||||
it[getPermissionLaunchCount(permission)] = appLaunchCounter + DELAY_LAUNCH_COUNT
|
||||
it[getPermissionDaysCount(permission)] = nowMillis + DELAY_DAYS_COUNT
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val DELAY_LAUNCH_COUNT = 5
|
||||
const val DELAY_DAYS_COUNT = 3L * 24 * 3600 * 1000 // 3 days in millis
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
package com.tangem.data.settings.di
|
||||
|
||||
import com.tangem.data.settings.DefaultAppRatingRepository
|
||||
import com.tangem.data.settings.DefaultSettingsRepository
|
||||
import com.tangem.data.settings.DefaultPermissionRepository
|
||||
import com.tangem.data.settings.DefaultPromoSettingsRepository
|
||||
import com.tangem.data.settings.DefaultSettingsRepository
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.settings.repositories.AppRatingRepository
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.domain.settings.repositories.PermissionRepository
|
||||
import com.tangem.domain.settings.repositories.PromoSettingsRepository
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -34,4 +36,10 @@ internal object SettingsDataModule {
|
|||
fun providePromoSettingsSettingsRepository(appPreferencesStore: AppPreferencesStore): PromoSettingsRepository {
|
||||
return DefaultPromoSettingsRepository(appPreferencesStore = appPreferencesStore)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePushPermissionRepository(appPreferencesStore: AppPreferencesStore): PermissionRepository {
|
||||
return DefaultPermissionRepository(appPreferencesStore = appPreferencesStore)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,12 +11,20 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.domain.staking)
|
||||
implementation(projects.features.staking.api)
|
||||
|
||||
/** Common modules */
|
||||
implementation(projects.data.common)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.staking)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** Feature Api modules */
|
||||
implementation(projects.features.staking.api)
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
|
|
@ -24,13 +32,17 @@ dependencies {
|
|||
// endregion
|
||||
|
||||
// region Others dependencies
|
||||
implementation(deps.androidx.datastore)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
|
||||
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(deps.tangem.blockchain) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.data.staking
|
||||
|
||||
import com.tangem.data.staking.converters.error.StakeKitErrorConverter
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.domain.staking.model.StakingError
|
||||
import com.tangem.domain.staking.repositories.StakingErrorResolver
|
||||
|
||||
internal class DefaultStakingErrorResolver(
|
||||
private val stakeKitErrorConverter: StakeKitErrorConverter,
|
||||
) : StakingErrorResolver {
|
||||
|
||||
override fun resolve(throwable: Throwable): StakingError {
|
||||
return if (throwable is ApiResponseError.HttpException) {
|
||||
stakeKitErrorConverter.convert(throwable.errorBody.orEmpty())
|
||||
} else {
|
||||
StakingError.UnknownError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +1,123 @@
|
|||
package com.tangem.data.staking
|
||||
|
||||
import arrow.core.raise.catch
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toCoinId
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.staking.converters.*
|
||||
import com.tangem.data.staking.converters.action.ActionStatusConverter
|
||||
import com.tangem.data.staking.converters.action.EnterActionResponseConverter
|
||||
import com.tangem.data.staking.converters.action.StakingActionTypeConverter
|
||||
import com.tangem.data.staking.converters.transaction.GasEstimateConverter
|
||||
import com.tangem.data.staking.converters.transaction.StakingTransactionConverter
|
||||
import com.tangem.data.staking.converters.transaction.StakingTransactionStatusConverter
|
||||
import com.tangem.data.staking.converters.transaction.StakingTransactionTypeConverter
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.StakingEntryInfo
|
||||
import com.tangem.datasource.api.stakekit.models.request.*
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectListSync
|
||||
import com.tangem.datasource.local.token.StakingBalanceStore
|
||||
import com.tangem.datasource.local.token.StakingYieldsStore
|
||||
import com.tangem.domain.core.lce.LceFlow
|
||||
import com.tangem.domain.core.lce.lceFlow
|
||||
import com.tangem.domain.staking.model.*
|
||||
import com.tangem.domain.staking.model.action.StakingAction
|
||||
import com.tangem.domain.staking.model.action.StakingActionCommonType
|
||||
import com.tangem.domain.staking.model.transaction.ActionParams
|
||||
import com.tangem.domain.staking.model.transaction.StakingGasEstimate
|
||||
import com.tangem.domain.staking.model.transaction.StakingTransaction
|
||||
import com.tangem.domain.staking.repositories.StakingRepository
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.toFormattedString
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Suppress("LargeClass", "LongParameterList")
|
||||
internal class DefaultStakingRepository(
|
||||
private val stakeKitApi: StakeKitApi,
|
||||
private val stakingFeatureToggles: StakingFeatureToggles,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val stakingYieldsStore: StakingYieldsStore,
|
||||
private val stakingBalanceStore: StakingBalanceStore,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val stakingFeatureToggle: StakingFeatureToggles,
|
||||
) : StakingRepository {
|
||||
|
||||
override fun getStakingAvailability(blockchainId: String): StakingAvailability {
|
||||
if (!stakingFeatureToggles.isStakingEnabled) {
|
||||
return StakingAvailability.Unavailable
|
||||
}
|
||||
private val stakingNetworkTypeConverter = StakingNetworkTypeConverter()
|
||||
private val networkTypeConverter = StakingNetworkTypeConverter()
|
||||
private val transactionStatusConverter = StakingTransactionStatusConverter()
|
||||
private val transactionTypeConverter = StakingTransactionTypeConverter()
|
||||
private val actionStatusConverter = ActionStatusConverter()
|
||||
private val stakingActionTypeConverter = StakingActionTypeConverter()
|
||||
private val tokenConverter = TokenConverter(
|
||||
stakingNetworkTypeConverter = stakingNetworkTypeConverter,
|
||||
)
|
||||
private val yieldConverter = YieldConverter(
|
||||
tokenConverter = tokenConverter,
|
||||
)
|
||||
private val gasEstimateConverter = GasEstimateConverter(
|
||||
tokenConverter = tokenConverter,
|
||||
)
|
||||
private val transactionConverter = StakingTransactionConverter(
|
||||
networkTypeConverter = networkTypeConverter,
|
||||
transactionStatusConverter = transactionStatusConverter,
|
||||
transactionTypeConverter = transactionTypeConverter,
|
||||
gasEstimateConverter = gasEstimateConverter,
|
||||
)
|
||||
private val enterActionResponseConverter = EnterActionResponseConverter(
|
||||
actionStatusConverter = actionStatusConverter,
|
||||
stakingActionTypeConverter = stakingActionTypeConverter,
|
||||
transactionConverter = transactionConverter,
|
||||
)
|
||||
|
||||
return integrationIdMap[Blockchain.fromId(blockchainId)]?.let {
|
||||
StakingAvailability.Available(it)
|
||||
} ?: StakingAvailability.Unavailable
|
||||
private val yieldBalanceConverter = YieldBalanceConverter()
|
||||
|
||||
private val yieldBalanceListConverter = YieldBalanceListConverter()
|
||||
|
||||
private val isYieldBalanceFetching = MutableStateFlow(
|
||||
value = emptyMap<UserWalletId, Boolean>(),
|
||||
)
|
||||
|
||||
override fun isStakingSupported(currencyId: String): Boolean {
|
||||
return integrationIdMap.containsKey(currencyId)
|
||||
}
|
||||
|
||||
override suspend fun fetchEnabledYields(refresh: Boolean) {
|
||||
withContext(dispatchers.io) {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = YIELDS_STORE_KEY,
|
||||
skipCache = refresh,
|
||||
block = {
|
||||
val stakingTokensWithYields = stakeKitApi.getMultipleYields().getOrThrow()
|
||||
stakingYieldsStore.store(stakingTokensWithYields.data)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield {
|
||||
return withContext(dispatchers.io) {
|
||||
val yields = getEnabledYields() ?: error("No yields found")
|
||||
val rawCurrencyId = cryptoCurrencyId.rawCurrencyId ?: error("Staking custom tokens is not available")
|
||||
|
||||
val prefetchedYield = findPrefetchedYield(
|
||||
yields = yields,
|
||||
currencyId = rawCurrencyId,
|
||||
symbol = symbol,
|
||||
)
|
||||
|
||||
prefetchedYield ?: error("Staking is unavailable")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo {
|
||||
|
|
@ -38,31 +132,374 @@ internal class DefaultStakingRepository(
|
|||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking"
|
||||
private const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking"
|
||||
private const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking"
|
||||
private const val ETHEREUM_INTEGRATION_ID = "ethereum-matic-native-staking"
|
||||
private const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking"
|
||||
private const val TRON_INTEGRATION_ID = "tron-trx-native-staking"
|
||||
private const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking"
|
||||
private const val BINANCE_INTEGRATION_ID = "binance-bnb-native-staking"
|
||||
private const val KAVA_INTEGRATION_ID = "kava-kava-native-staking"
|
||||
private const val NEAR_INTEGRATION_ID = "near-near-native-staking"
|
||||
private const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking"
|
||||
override suspend fun getStakingAvailabilityForActions(
|
||||
cryptoCurrencyId: CryptoCurrency.ID,
|
||||
symbol: String,
|
||||
): StakingAvailability {
|
||||
val rawCurrencyId = cryptoCurrencyId.rawCurrencyId ?: return StakingAvailability.Unavailable
|
||||
|
||||
private val integrationIdMap = mapOf(
|
||||
Blockchain.Solana to SOLANA_INTEGRATION_ID,
|
||||
Blockchain.Cosmos to COSMOS_INTEGRATION_ID,
|
||||
Blockchain.Polkadot to POLKADOT_INTEGRATION_ID,
|
||||
Blockchain.Polygon to ETHEREUM_INTEGRATION_ID,
|
||||
Blockchain.Avalanche to AVALANCHE_INTEGRATION_ID,
|
||||
Blockchain.Tron to TRON_INTEGRATION_ID,
|
||||
Blockchain.Cronos to CRONOS_INTEGRATION_ID,
|
||||
Blockchain.Binance to BINANCE_INTEGRATION_ID,
|
||||
Blockchain.Kava to KAVA_INTEGRATION_ID,
|
||||
Blockchain.Near to NEAR_INTEGRATION_ID,
|
||||
Blockchain.Tezos to TEZOS_INTEGRATION_ID,
|
||||
return withContext(dispatchers.io) {
|
||||
val yields = getEnabledYields() ?: return@withContext StakingAvailability.Unavailable
|
||||
|
||||
val prefetchedYield = findPrefetchedYield(yields, rawCurrencyId, symbol)
|
||||
val isSupported = isStakingSupported(rawCurrencyId)
|
||||
|
||||
when {
|
||||
prefetchedYield != null && isSupported -> {
|
||||
StakingAvailability.Available(prefetchedYield.id)
|
||||
}
|
||||
prefetchedYield == null && isSupported -> {
|
||||
StakingAvailability.TemporaryDisabled
|
||||
}
|
||||
else -> StakingAvailability.Unavailable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createAction(params: ActionParams): StakingAction {
|
||||
return withContext(dispatchers.io) {
|
||||
val body = createActionRequestBody(params)
|
||||
val response = when (params.actionCommonType) {
|
||||
StakingActionCommonType.ENTER -> stakeKitApi.createEnterAction(body)
|
||||
StakingActionCommonType.EXIT -> stakeKitApi.createExitAction(body)
|
||||
}
|
||||
|
||||
enterActionResponseConverter.convert(response.getOrThrow())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun estimateGas(params: ActionParams): StakingGasEstimate {
|
||||
return withContext(dispatchers.io) {
|
||||
val body = createActionRequestBody(params)
|
||||
|
||||
val gasEstimateDTO = when (params.actionCommonType) {
|
||||
StakingActionCommonType.ENTER -> stakeKitApi.estimateGasOnEnter(body)
|
||||
StakingActionCommonType.EXIT -> stakeKitApi.estimateGasOnExit(body)
|
||||
}
|
||||
|
||||
gasEstimateConverter.convert(gasEstimateDTO.getOrThrow())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun constructTransaction(transactionId: String): StakingTransaction {
|
||||
return withContext(dispatchers.io) {
|
||||
val transactionResponse = stakeKitApi.constructTransaction(
|
||||
transactionId = transactionId,
|
||||
body = ConstructTransactionRequestBody(),
|
||||
)
|
||||
|
||||
transactionConverter.convert(transactionResponse.getOrThrow())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun fetchSingleYieldBalance(
|
||||
userWalletId: UserWalletId,
|
||||
address: CryptoCurrencyAddress,
|
||||
refresh: Boolean,
|
||||
) = withContext(dispatchers.io) {
|
||||
if (!stakingFeatureToggle.isStakingEnabled) return@withContext
|
||||
|
||||
val cryptoCurrency = address.cryptoCurrency
|
||||
val rawCurrencyId =
|
||||
cryptoCurrency.id.rawCurrencyId ?: error("Staking custom tokens is not available")
|
||||
|
||||
val integrationId = integrationIdMap[rawCurrencyId] ?: return@withContext
|
||||
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getYieldBalancesKey(userWalletId),
|
||||
skipCache = refresh,
|
||||
block = {
|
||||
val requestBody = getBalanceRequestData(address.address, integrationId)
|
||||
val result = stakeKitApi.getSingleYieldBalance(
|
||||
integrationId = requestBody.integrationId,
|
||||
body = requestBody,
|
||||
).getOrThrow()
|
||||
|
||||
stakingBalanceStore.store(
|
||||
requestBody.integrationId,
|
||||
YieldBalanceWrapperDTO(
|
||||
balances = result,
|
||||
integrationId = requestBody.integrationId,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun getSingleYieldBalanceFlow(
|
||||
userWalletId: UserWalletId,
|
||||
address: CryptoCurrencyAddress,
|
||||
): Flow<YieldBalance> = channelFlow {
|
||||
if (!stakingFeatureToggle.isStakingEnabled) {
|
||||
send(YieldBalance.Empty)
|
||||
} else {
|
||||
launch(dispatchers.io) {
|
||||
val integrationId = integrationIdMap[address.cryptoCurrency.id.rawCurrencyId]
|
||||
?: error("Could not get integrationId")
|
||||
stakingBalanceStore.get(integrationId)
|
||||
.collectLatest {
|
||||
send(
|
||||
yieldBalanceConverter.convert(
|
||||
YieldBalanceConverter.Data(
|
||||
balance = it,
|
||||
integrationId = integrationId,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
withContext(dispatchers.io) {
|
||||
fetchSingleYieldBalance(
|
||||
userWalletId,
|
||||
address,
|
||||
)
|
||||
}
|
||||
}
|
||||
}.cancellable()
|
||||
|
||||
override suspend fun getSingleYieldBalanceSync(
|
||||
userWalletId: UserWalletId,
|
||||
address: CryptoCurrencyAddress,
|
||||
): YieldBalance = withContext(dispatchers.io) {
|
||||
if (!stakingFeatureToggle.isStakingEnabled) {
|
||||
YieldBalance.Empty
|
||||
} else {
|
||||
fetchSingleYieldBalance(userWalletId, address)
|
||||
|
||||
val integrationId = integrationIdMap[address.cryptoCurrency.id.rawCurrencyId]
|
||||
?: error("Could not get integrationId")
|
||||
val result = stakingBalanceStore.getSyncOrNull(integrationId) ?: return@withContext YieldBalance.Error
|
||||
yieldBalanceConverter.convert(
|
||||
YieldBalanceConverter.Data(
|
||||
balance = result,
|
||||
integrationId = integrationId,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun fetchMultiYieldBalance(
|
||||
userWalletId: UserWalletId,
|
||||
addresses: List<CryptoCurrencyAddress>,
|
||||
refresh: Boolean,
|
||||
) = withContext(dispatchers.io) {
|
||||
if (!stakingFeatureToggle.isStakingEnabled) return@withContext
|
||||
try {
|
||||
isYieldBalanceFetching.update {
|
||||
it + (userWalletId to true)
|
||||
}
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getYieldBalancesKey(userWalletId),
|
||||
skipCache = refresh,
|
||||
block = {
|
||||
val result = stakeKitApi.getMultipleYieldBalances(
|
||||
addresses
|
||||
.mapNotNull { networkAddress ->
|
||||
val cryptoCurrency = networkAddress.cryptoCurrency
|
||||
val rawCurrencyId = cryptoCurrency.id.rawCurrencyId ?: error("Currency raw id is null")
|
||||
val integrationId = integrationIdMap[rawCurrencyId]
|
||||
|
||||
if (integrationId != null) {
|
||||
networkAddress.address to integrationId
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.distinct()
|
||||
.map { getBalanceRequestData(it.first, it.second) },
|
||||
).getOrThrow()
|
||||
|
||||
stakingBalanceStore.store(result)
|
||||
},
|
||||
)
|
||||
} finally {
|
||||
isYieldBalanceFetching.update {
|
||||
it - userWalletId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMultiYieldBalanceFlow(
|
||||
userWalletId: UserWalletId,
|
||||
addresses: List<CryptoCurrencyAddress>,
|
||||
): Flow<YieldBalanceList> = channelFlow {
|
||||
if (!stakingFeatureToggle.isStakingEnabled) {
|
||||
send(YieldBalanceList.Empty)
|
||||
} else {
|
||||
launch(dispatchers.io) {
|
||||
stakingBalanceStore.get()
|
||||
.collectLatest { send(yieldBalanceListConverter.convert(it)) }
|
||||
}
|
||||
|
||||
withContext(dispatchers.io) {
|
||||
fetchMultiYieldBalance(
|
||||
userWalletId,
|
||||
addresses,
|
||||
)
|
||||
}
|
||||
}
|
||||
}.cancellable()
|
||||
|
||||
override fun getMultiYieldBalanceLce(
|
||||
userWalletId: UserWalletId,
|
||||
addresses: List<CryptoCurrencyAddress>,
|
||||
): LceFlow<Throwable, YieldBalanceList> = lceFlow {
|
||||
if (!stakingFeatureToggle.isStakingEnabled) {
|
||||
send(YieldBalanceList.Empty)
|
||||
} else {
|
||||
launch(dispatchers.io) {
|
||||
combine(
|
||||
stakingBalanceStore.get(),
|
||||
isYieldBalanceFetching.map { it.getOrElse(userWalletId) { false } },
|
||||
) { result, isFetching ->
|
||||
val balances = yieldBalanceListConverter.convert(result)
|
||||
send(balances, isStillLoading = isFetching)
|
||||
}.collect()
|
||||
}
|
||||
withContext(dispatchers.io) {
|
||||
catch(
|
||||
block = { fetchMultiYieldBalance(userWalletId, addresses, refresh = false) },
|
||||
catch = { raise(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getMultiYieldBalanceSync(
|
||||
userWalletId: UserWalletId,
|
||||
addresses: List<CryptoCurrencyAddress>,
|
||||
): YieldBalanceList = withContext(dispatchers.io) {
|
||||
if (!stakingFeatureToggle.isStakingEnabled) {
|
||||
YieldBalanceList.Empty
|
||||
} else {
|
||||
fetchMultiYieldBalance(userWalletId, addresses)
|
||||
val result = stakingBalanceStore.getSyncOrNull() ?: return@withContext YieldBalanceList.Error
|
||||
yieldBalanceListConverter.convert(result)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun submitHash(transactionId: String, transactionHash: String) {
|
||||
withContext(dispatchers.io) {
|
||||
stakeKitApi.submitTransactionHash(
|
||||
transactionId = transactionId,
|
||||
body = SubmitTransactionHashRequestBody(
|
||||
hash = transactionHash,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeUnsubmittedHash(unsubmittedTransactionMetadata: UnsubmittedTransactionMetadata) {
|
||||
withContext(dispatchers.io) {
|
||||
appPreferencesStore.editData { preferences ->
|
||||
val savedTransactions = preferences.getObjectListOrDefault<UnsubmittedTransactionMetadata>(
|
||||
key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY,
|
||||
default = emptyList(),
|
||||
)
|
||||
|
||||
preferences.setObjectList(
|
||||
key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY,
|
||||
value = savedTransactions + unsubmittedTransactionMetadata,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendUnsubmittedHashes() {
|
||||
withContext(NonCancellable) {
|
||||
val savedTransactions = appPreferencesStore.getObjectListSync<UnsubmittedTransactionMetadata>(
|
||||
key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY,
|
||||
)
|
||||
|
||||
savedTransactions.forEach {
|
||||
stakeKitApi.submitTransactionHash(
|
||||
transactionId = it.transactionId,
|
||||
body = SubmitTransactionHashRequestBody(hash = it.transactionHash),
|
||||
)
|
||||
}
|
||||
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
mutablePreferences.setObjectList<UnsubmittedTransactionMetadata>(
|
||||
key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY,
|
||||
value = emptyList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createActionRequestBody(params: ActionParams): ActionRequestBody {
|
||||
return ActionRequestBody(
|
||||
integrationId = params.integrationId,
|
||||
addresses = Address(params.address),
|
||||
args = ActionRequestBody.EnterActionRequestBodyArgs(
|
||||
amount = params.amount.toFormattedString(params.token.decimals),
|
||||
inputToken = tokenConverter.convertBack(params.token),
|
||||
validatorAddress = params.validatorAddress,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun isStakeMoreAvailable(networkId: Network.ID): Boolean {
|
||||
val blockchain = Blockchain.fromId(networkId.value)
|
||||
return when (blockchain) {
|
||||
Blockchain.Solana -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun findPrefetchedYield(yields: List<Yield>, currencyId: String, symbol: String): Yield? {
|
||||
return yields.find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol }
|
||||
}
|
||||
|
||||
private suspend fun getEnabledYields(): List<Yield>? {
|
||||
val yields = stakingYieldsStore.getSyncOrNull() ?: return null
|
||||
return yields.map { yieldConverter.convert(it) }
|
||||
}
|
||||
|
||||
private fun getBalanceRequestData(address: String, integrationId: String): YieldBalanceRequestBody {
|
||||
return YieldBalanceRequestBody(
|
||||
addresses = Address(
|
||||
address = address,
|
||||
additionalAddresses = null, // todo fill additional addresses metadata if needed
|
||||
explorerUrl = "", // todo fill exporer url [REDACTED_JIRA]
|
||||
),
|
||||
args = YieldBalanceRequestBody.YieldBalanceRequestArgs(
|
||||
validatorAddresses = listOf(), // todo add validators [REDACTED_JIRA]
|
||||
),
|
||||
integrationId = integrationId,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getYieldBalancesKey(userWalletId: UserWalletId) = "yield_balance_${userWalletId.stringValue}"
|
||||
|
||||
private companion object {
|
||||
const val YIELDS_STORE_KEY = "yields"
|
||||
|
||||
const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking"
|
||||
const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking"
|
||||
const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking"
|
||||
const val ETHEREUM_INTEGRATION_ID = "ethereum-matic-native-staking"
|
||||
const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking"
|
||||
const val TRON_INTEGRATION_ID = "tron-trx-native-staking"
|
||||
const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking"
|
||||
const val BINANCE_INTEGRATION_ID = "binance-bnb-native-staking"
|
||||
const val KAVA_INTEGRATION_ID = "kava-kava-native-staking"
|
||||
const val NEAR_INTEGRATION_ID = "near-near-native-staking"
|
||||
const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking"
|
||||
|
||||
val integrationIdMap = mapOf(
|
||||
Blockchain.Solana.toCoinId() to SOLANA_INTEGRATION_ID,
|
||||
Blockchain.Cosmos.toCoinId() to COSMOS_INTEGRATION_ID,
|
||||
Blockchain.Polkadot.toCoinId() to POLKADOT_INTEGRATION_ID,
|
||||
Blockchain.Polygon.toCoinId() to ETHEREUM_INTEGRATION_ID,
|
||||
Blockchain.Avalanche.toCoinId() to AVALANCHE_INTEGRATION_ID,
|
||||
Blockchain.Tron.toCoinId() to TRON_INTEGRATION_ID,
|
||||
Blockchain.Cronos.toCoinId() to CRONOS_INTEGRATION_ID,
|
||||
Blockchain.Binance.toCoinId() to BINANCE_INTEGRATION_ID,
|
||||
Blockchain.Kava.toCoinId() to KAVA_INTEGRATION_ID,
|
||||
Blockchain.Near.toCoinId() to NEAR_INTEGRATION_ID,
|
||||
Blockchain.Tezos.toCoinId() to TEZOS_INTEGRATION_ID,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
package com.tangem.data.staking.converters
|
||||
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO
|
||||
import com.tangem.domain.staking.model.NetworkType
|
||||
import com.tangem.utils.converter.TwoWayConverter
|
||||
|
||||
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
||||
class StakingNetworkTypeConverter : TwoWayConverter<NetworkTypeDTO, NetworkType> {
|
||||
|
||||
override fun convert(value: NetworkTypeDTO): NetworkType {
|
||||
return when (value) {
|
||||
NetworkTypeDTO.AVALANCHE_C -> NetworkType.AVALANCHE_C
|
||||
NetworkTypeDTO.AVALANCHE_ATOMIC -> NetworkType.AVALANCHE_ATOMIC
|
||||
NetworkTypeDTO.AVALANCHE_P -> NetworkType.AVALANCHE_P
|
||||
NetworkTypeDTO.ARBITRUM -> NetworkType.ARBITRUM
|
||||
NetworkTypeDTO.BINANCE -> NetworkType.BINANCE
|
||||
NetworkTypeDTO.CELO -> NetworkType.CELO
|
||||
NetworkTypeDTO.ETHEREUM -> NetworkType.ETHEREUM
|
||||
NetworkTypeDTO.ETHEREUM_GOERLI -> NetworkType.ETHEREUM_GOERLI
|
||||
NetworkTypeDTO.ETHEREUM_HOLESKY -> NetworkType.ETHEREUM_HOLESKY
|
||||
NetworkTypeDTO.FANTOM -> NetworkType.FANTOM
|
||||
NetworkTypeDTO.HARMONY -> NetworkType.HARMONY
|
||||
NetworkTypeDTO.OPTIMISM -> NetworkType.OPTIMISM
|
||||
NetworkTypeDTO.POLYGON -> NetworkType.POLYGON
|
||||
NetworkTypeDTO.GNOSIS -> NetworkType.GNOSIS
|
||||
NetworkTypeDTO.MOONRIVER -> NetworkType.MOONRIVER
|
||||
NetworkTypeDTO.OKC -> NetworkType.OKC
|
||||
NetworkTypeDTO.ZKSYNC -> NetworkType.ZKSYNC
|
||||
NetworkTypeDTO.VICTION -> NetworkType.VICTION
|
||||
NetworkTypeDTO.AGORIC -> NetworkType.AGORIC
|
||||
NetworkTypeDTO.AKASH -> NetworkType.AKASH
|
||||
NetworkTypeDTO.AXELAR -> NetworkType.AXELAR
|
||||
NetworkTypeDTO.BAND_PROTOCOL -> NetworkType.BAND_PROTOCOL
|
||||
NetworkTypeDTO.BITSONG -> NetworkType.BITSONG
|
||||
NetworkTypeDTO.CANTO -> NetworkType.CANTO
|
||||
NetworkTypeDTO.CHIHUAHUA -> NetworkType.CHIHUAHUA
|
||||
NetworkTypeDTO.COMDEX -> NetworkType.COMDEX
|
||||
NetworkTypeDTO.COREUM -> NetworkType.COREUM
|
||||
NetworkTypeDTO.COSMOS -> NetworkType.COSMOS
|
||||
NetworkTypeDTO.CRESCENT -> NetworkType.CRESCENT
|
||||
NetworkTypeDTO.CRONOS -> NetworkType.CRONOS
|
||||
NetworkTypeDTO.CUDOS -> NetworkType.CUDOS
|
||||
NetworkTypeDTO.DESMOS -> NetworkType.DESMOS
|
||||
NetworkTypeDTO.DYDX -> NetworkType.DYDX
|
||||
NetworkTypeDTO.EVMOS -> NetworkType.EVMOS
|
||||
NetworkTypeDTO.FETCH_AI -> NetworkType.FETCH_AI
|
||||
NetworkTypeDTO.GRAVITY_BRIDGE -> NetworkType.GRAVITY_BRIDGE
|
||||
NetworkTypeDTO.INJECTIVE -> NetworkType.INJECTIVE
|
||||
NetworkTypeDTO.IRISNET -> NetworkType.IRISNET
|
||||
NetworkTypeDTO.JUNO -> NetworkType.JUNO
|
||||
NetworkTypeDTO.KAVA -> NetworkType.KAVA
|
||||
NetworkTypeDTO.KI_NETWORK -> NetworkType.KI_NETWORK
|
||||
NetworkTypeDTO.MARS_PROTOCOL -> NetworkType.MARS_PROTOCOL
|
||||
NetworkTypeDTO.NYM -> NetworkType.NYM
|
||||
NetworkTypeDTO.OKEX_CHAIN -> NetworkType.OKEX_CHAIN
|
||||
NetworkTypeDTO.ONOMY -> NetworkType.ONOMY
|
||||
NetworkTypeDTO.OSMOSIS -> NetworkType.OSMOSIS
|
||||
NetworkTypeDTO.PERSISTENCE -> NetworkType.PERSISTENCE
|
||||
NetworkTypeDTO.QUICKSILVER -> NetworkType.QUICKSILVER
|
||||
NetworkTypeDTO.REGEN -> NetworkType.REGEN
|
||||
NetworkTypeDTO.SECRET -> NetworkType.SECRET
|
||||
NetworkTypeDTO.SENTINEL -> NetworkType.SENTINEL
|
||||
NetworkTypeDTO.SOMMELIER -> NetworkType.SOMMELIER
|
||||
NetworkTypeDTO.STAFI -> NetworkType.STAFI
|
||||
NetworkTypeDTO.STARGAZE -> NetworkType.STARGAZE
|
||||
NetworkTypeDTO.STRIDE -> NetworkType.STRIDE
|
||||
NetworkTypeDTO.TERITORI -> NetworkType.TERITORI
|
||||
NetworkTypeDTO.TGRADE -> NetworkType.TGRADE
|
||||
NetworkTypeDTO.UMEE -> NetworkType.UMEE
|
||||
NetworkTypeDTO.POLKADOT -> NetworkType.POLKADOT
|
||||
NetworkTypeDTO.KUSAMA -> NetworkType.KUSAMA
|
||||
NetworkTypeDTO.WESTEND -> NetworkType.WESTEND
|
||||
NetworkTypeDTO.BINANCEBEACON -> NetworkType.BINANCEBEACON
|
||||
NetworkTypeDTO.NEAR -> NetworkType.NEAR
|
||||
NetworkTypeDTO.SOLANA -> NetworkType.SOLANA
|
||||
NetworkTypeDTO.TEZOS -> NetworkType.TEZOS
|
||||
NetworkTypeDTO.TRON -> NetworkType.TRON
|
||||
else -> NetworkType.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
override fun convertBack(value: NetworkType): NetworkTypeDTO {
|
||||
return when (value) {
|
||||
NetworkType.AVALANCHE_C -> NetworkTypeDTO.AVALANCHE_C
|
||||
NetworkType.AVALANCHE_ATOMIC -> NetworkTypeDTO.AVALANCHE_ATOMIC
|
||||
NetworkType.AVALANCHE_P -> NetworkTypeDTO.AVALANCHE_P
|
||||
NetworkType.ARBITRUM -> NetworkTypeDTO.ARBITRUM
|
||||
NetworkType.BINANCE -> NetworkTypeDTO.BINANCE
|
||||
NetworkType.CELO -> NetworkTypeDTO.CELO
|
||||
NetworkType.ETHEREUM -> NetworkTypeDTO.ETHEREUM
|
||||
NetworkType.ETHEREUM_GOERLI -> NetworkTypeDTO.ETHEREUM_GOERLI
|
||||
NetworkType.ETHEREUM_HOLESKY -> NetworkTypeDTO.ETHEREUM_HOLESKY
|
||||
NetworkType.FANTOM -> NetworkTypeDTO.FANTOM
|
||||
NetworkType.HARMONY -> NetworkTypeDTO.HARMONY
|
||||
NetworkType.OPTIMISM -> NetworkTypeDTO.OPTIMISM
|
||||
NetworkType.POLYGON -> NetworkTypeDTO.POLYGON
|
||||
NetworkType.GNOSIS -> NetworkTypeDTO.GNOSIS
|
||||
NetworkType.MOONRIVER -> NetworkTypeDTO.MOONRIVER
|
||||
NetworkType.OKC -> NetworkTypeDTO.OKC
|
||||
NetworkType.ZKSYNC -> NetworkTypeDTO.ZKSYNC
|
||||
NetworkType.VICTION -> NetworkTypeDTO.VICTION
|
||||
NetworkType.AGORIC -> NetworkTypeDTO.AGORIC
|
||||
NetworkType.AKASH -> NetworkTypeDTO.AKASH
|
||||
NetworkType.AXELAR -> NetworkTypeDTO.AXELAR
|
||||
NetworkType.BAND_PROTOCOL -> NetworkTypeDTO.BAND_PROTOCOL
|
||||
NetworkType.BITSONG -> NetworkTypeDTO.BITSONG
|
||||
NetworkType.CANTO -> NetworkTypeDTO.CANTO
|
||||
NetworkType.CHIHUAHUA -> NetworkTypeDTO.CHIHUAHUA
|
||||
NetworkType.COMDEX -> NetworkTypeDTO.COMDEX
|
||||
NetworkType.COREUM -> NetworkTypeDTO.COREUM
|
||||
NetworkType.COSMOS -> NetworkTypeDTO.COSMOS
|
||||
NetworkType.CRESCENT -> NetworkTypeDTO.CRESCENT
|
||||
NetworkType.CRONOS -> NetworkTypeDTO.CRONOS
|
||||
NetworkType.CUDOS -> NetworkTypeDTO.CUDOS
|
||||
NetworkType.DESMOS -> NetworkTypeDTO.DESMOS
|
||||
NetworkType.DYDX -> NetworkTypeDTO.DYDX
|
||||
NetworkType.EVMOS -> NetworkTypeDTO.EVMOS
|
||||
NetworkType.FETCH_AI -> NetworkTypeDTO.FETCH_AI
|
||||
NetworkType.GRAVITY_BRIDGE -> NetworkTypeDTO.GRAVITY_BRIDGE
|
||||
NetworkType.INJECTIVE -> NetworkTypeDTO.INJECTIVE
|
||||
NetworkType.IRISNET -> NetworkTypeDTO.IRISNET
|
||||
NetworkType.JUNO -> NetworkTypeDTO.JUNO
|
||||
NetworkType.KAVA -> NetworkTypeDTO.KAVA
|
||||
NetworkType.KI_NETWORK -> NetworkTypeDTO.KI_NETWORK
|
||||
NetworkType.MARS_PROTOCOL -> NetworkTypeDTO.MARS_PROTOCOL
|
||||
NetworkType.NYM -> NetworkTypeDTO.NYM
|
||||
NetworkType.OKEX_CHAIN -> NetworkTypeDTO.OKEX_CHAIN
|
||||
NetworkType.ONOMY -> NetworkTypeDTO.ONOMY
|
||||
NetworkType.OSMOSIS -> NetworkTypeDTO.OSMOSIS
|
||||
NetworkType.PERSISTENCE -> NetworkTypeDTO.PERSISTENCE
|
||||
NetworkType.QUICKSILVER -> NetworkTypeDTO.QUICKSILVER
|
||||
NetworkType.REGEN -> NetworkTypeDTO.REGEN
|
||||
NetworkType.SECRET -> NetworkTypeDTO.SECRET
|
||||
NetworkType.SENTINEL -> NetworkTypeDTO.SENTINEL
|
||||
NetworkType.SOMMELIER -> NetworkTypeDTO.SOMMELIER
|
||||
NetworkType.STAFI -> NetworkTypeDTO.STAFI
|
||||
NetworkType.STARGAZE -> NetworkTypeDTO.STARGAZE
|
||||
NetworkType.STRIDE -> NetworkTypeDTO.STRIDE
|
||||
NetworkType.TERITORI -> NetworkTypeDTO.TERITORI
|
||||
NetworkType.TGRADE -> NetworkTypeDTO.TGRADE
|
||||
NetworkType.UMEE -> NetworkTypeDTO.UMEE
|
||||
NetworkType.POLKADOT -> NetworkTypeDTO.POLKADOT
|
||||
NetworkType.KUSAMA -> NetworkTypeDTO.KUSAMA
|
||||
NetworkType.WESTEND -> NetworkTypeDTO.WESTEND
|
||||
NetworkType.BINANCEBEACON -> NetworkTypeDTO.BINANCEBEACON
|
||||
NetworkType.NEAR -> NetworkTypeDTO.NEAR
|
||||
NetworkType.SOLANA -> NetworkTypeDTO.SOLANA
|
||||
NetworkType.TEZOS -> NetworkTypeDTO.TEZOS
|
||||
NetworkType.TRON -> NetworkTypeDTO.TRON
|
||||
else -> NetworkTypeDTO.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.data.staking.converters
|
||||
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYieldDTO
|
||||
import com.tangem.domain.staking.model.StakingToken
|
||||
import com.tangem.domain.staking.model.StakingTokenWithYield
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
class StakingTokenConverter : Converter<TokenWithYieldDTO, StakingTokenWithYield> {
|
||||
|
||||
override fun convert(value: TokenWithYieldDTO): StakingTokenWithYield {
|
||||
return StakingTokenWithYield(
|
||||
token = StakingToken(
|
||||
name = value.token.name,
|
||||
symbol = value.token.symbol,
|
||||
decimals = value.token.decimals,
|
||||
contractAddress = value.token.address,
|
||||
coinGeckoId = value.token.coinGeckoId,
|
||||
),
|
||||
availableYieldIds = value.availableYieldIds,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.data.staking.converters
|
||||
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO
|
||||
import com.tangem.domain.staking.model.Token
|
||||
import com.tangem.utils.converter.TwoWayConverter
|
||||
|
||||
class TokenConverter(
|
||||
private val stakingNetworkTypeConverter: StakingNetworkTypeConverter,
|
||||
) : TwoWayConverter<TokenDTO, Token> {
|
||||
|
||||
override fun convert(value: TokenDTO): Token {
|
||||
return Token(
|
||||
name = value.name,
|
||||
network = stakingNetworkTypeConverter.convert(value.network),
|
||||
symbol = value.symbol,
|
||||
decimals = value.decimals,
|
||||
address = value.address,
|
||||
coinGeckoId = value.coinGeckoId,
|
||||
logoURI = value.logoURI,
|
||||
isPoints = value.isPoints,
|
||||
)
|
||||
}
|
||||
|
||||
override fun convertBack(value: Token): TokenDTO {
|
||||
return TokenDTO(
|
||||
name = value.name,
|
||||
network = stakingNetworkTypeConverter.convertBack(value.network),
|
||||
symbol = value.symbol,
|
||||
decimals = value.decimals,
|
||||
address = value.address,
|
||||
coinGeckoId = value.coinGeckoId,
|
||||
logoURI = value.logoURI,
|
||||
isPoints = value.isPoints,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.data.staking.converters
|
||||
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
|
||||
import com.tangem.domain.staking.model.BalanceItem
|
||||
import com.tangem.domain.staking.model.BalanceType
|
||||
import com.tangem.domain.staking.model.YieldBalance
|
||||
import com.tangem.domain.staking.model.YieldBalanceItem
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class YieldBalanceConverter : Converter<YieldBalanceConverter.Data, YieldBalance> {
|
||||
|
||||
override fun convert(value: Data): YieldBalance {
|
||||
return if (value.balance.isEmpty()) {
|
||||
YieldBalance.Empty
|
||||
} else {
|
||||
YieldBalance.Data(
|
||||
balance = YieldBalanceItem(
|
||||
items = value.balance.map { item ->
|
||||
BalanceItem(
|
||||
type = BalanceType.valueOf(item.type.name),
|
||||
amount = item.amount,
|
||||
pricePerShare = item.pricePerShare,
|
||||
rawCurrencyId = item.tokenDTO.coinGeckoId,
|
||||
validatorAddress = item.validatorAddress,
|
||||
)
|
||||
},
|
||||
integrationId = value.integrationId,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Data(
|
||||
val balance: List<BalanceDTO>,
|
||||
val integrationId: String?,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.data.staking.converters
|
||||
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
import com.tangem.domain.staking.model.YieldBalanceList
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class YieldBalanceListConverter : Converter<List<YieldBalanceWrapperDTO>, YieldBalanceList> {
|
||||
|
||||
internal val converter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
YieldBalanceConverter()
|
||||
}
|
||||
|
||||
override fun convert(value: List<YieldBalanceWrapperDTO>): YieldBalanceList {
|
||||
return if (value.isEmpty()) {
|
||||
YieldBalanceList.Empty
|
||||
} else {
|
||||
YieldBalanceList.Data(
|
||||
balances = value.map {
|
||||
converter.convert(
|
||||
YieldBalanceConverter.Data(
|
||||
balance = it.balances,
|
||||
integrationId = it.integrationId,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
package com.tangem.data.staking.converters
|
||||
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.AddressArgumentDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
|
||||
import com.tangem.domain.staking.model.*
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
class YieldConverter(
|
||||
private val tokenConverter: TokenConverter,
|
||||
) : Converter<YieldDTO, Yield> {
|
||||
|
||||
override fun convert(value: YieldDTO): Yield {
|
||||
return Yield(
|
||||
id = value.id,
|
||||
token = tokenConverter.convert(value.token),
|
||||
tokens = value.tokens.map { tokenConverter.convert(it) },
|
||||
args = convertArgs(value.args),
|
||||
status = convertStatus(value.status),
|
||||
apy = value.apy,
|
||||
rewardRate = value.rewardRate,
|
||||
rewardType = convertRewardType(value.rewardType),
|
||||
metadata = convertMetadata(value.metadata),
|
||||
validators = value.validators
|
||||
.filter { it.preferred }
|
||||
.map { convertValidator(it) }
|
||||
.sortedByDescending { it.apr },
|
||||
isAvailable = value.isAvailable,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertArgs(argsDTO: YieldDTO.ArgsDTO): Yield.Args {
|
||||
return Yield.Args(
|
||||
enter = convertEnter(argsDTO.enter),
|
||||
exit = argsDTO.exit?.let { convertEnter(it) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertEnter(enterDTO: YieldDTO.ArgsDTO.Enter): Yield.Args.Enter {
|
||||
return Yield.Args.Enter(
|
||||
addresses = convertAddresses(enterDTO.addresses),
|
||||
args = enterDTO.args.mapValues { convertAddressArgument(it.value) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertAddresses(addressesDTO: YieldDTO.ArgsDTO.Enter.Addresses): Yield.Args.Enter.Addresses {
|
||||
return Yield.Args.Enter.Addresses(
|
||||
address = convertAddressArgument(addressesDTO.address),
|
||||
additionalAddresses = addressesDTO.additionalAddresses?.mapValues { convertAddressArgument(it.value) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertAddressArgument(addressArgumentDTO: AddressArgumentDTO): AddressArgument {
|
||||
return AddressArgument(
|
||||
required = addressArgumentDTO.required,
|
||||
network = addressArgumentDTO.network,
|
||||
minimum = addressArgumentDTO.minimum,
|
||||
maximum = addressArgumentDTO.maximum,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertStatus(statusDTO: YieldDTO.StatusDTO): Yield.Status {
|
||||
return Yield.Status(
|
||||
enter = statusDTO.enter,
|
||||
exit = statusDTO.exit,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertMetadata(metadataDTO: YieldDTO.MetadataDTO): Yield.Metadata {
|
||||
return Yield.Metadata(
|
||||
name = metadataDTO.name,
|
||||
logoUri = metadataDTO.logoUri,
|
||||
description = metadataDTO.description,
|
||||
documentation = metadataDTO.documentation,
|
||||
gasFeeToken = tokenConverter.convert(metadataDTO.gasFeeTokenDTO),
|
||||
token = tokenConverter.convert(metadataDTO.tokenDTO),
|
||||
tokens = metadataDTO.tokensDTO.map { tokenConverter.convert(it) },
|
||||
type = metadataDTO.type,
|
||||
rewardSchedule = metadataDTO.rewardSchedule,
|
||||
cooldownPeriod = convertPeriod(metadataDTO.cooldownPeriod),
|
||||
warmupPeriod = convertPeriod(metadataDTO.warmupPeriod),
|
||||
rewardClaiming = metadataDTO.rewardClaiming,
|
||||
defaultValidator = metadataDTO.defaultValidator,
|
||||
minimumStake = metadataDTO.minimumStake,
|
||||
supportsMultipleValidators = metadataDTO.supportsMultipleValidators,
|
||||
revshare = convertEnabled(metadataDTO.revshare),
|
||||
fee = convertEnabled(metadataDTO.fee),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertPeriod(periodDTO: YieldDTO.MetadataDTO.PeriodDTO): Yield.Metadata.Period {
|
||||
return Yield.Metadata.Period(
|
||||
days = periodDTO.days,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertEnabled(enabledDTO: YieldDTO.MetadataDTO.EnabledDTO): Yield.Metadata.Enabled {
|
||||
return Yield.Metadata.Enabled(
|
||||
enabled = enabledDTO.enabled,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertValidator(validatorDTO: YieldDTO.ValidatorDTO): Yield.Validator {
|
||||
return Yield.Validator(
|
||||
address = validatorDTO.address,
|
||||
status = validatorDTO.status,
|
||||
name = validatorDTO.name,
|
||||
image = validatorDTO.image,
|
||||
website = validatorDTO.website,
|
||||
apr = validatorDTO.apr,
|
||||
commission = validatorDTO.commission,
|
||||
stakedBalance = validatorDTO.stakedBalance,
|
||||
votingPower = validatorDTO.votingPower,
|
||||
preferred = validatorDTO.preferred,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertRewardType(rewardTypeDTO: YieldDTO.RewardTypeDTO): Yield.RewardType {
|
||||
return when (rewardTypeDTO) {
|
||||
YieldDTO.RewardTypeDTO.APY -> Yield.RewardType.APY
|
||||
YieldDTO.RewardTypeDTO.APR -> Yield.RewardType.APR
|
||||
else -> Yield.RewardType.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.data.staking.converters.action
|
||||
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO
|
||||
import com.tangem.domain.staking.model.action.StakingActionStatus
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
class ActionStatusConverter : Converter<StakingActionStatusDTO, StakingActionStatus> {
|
||||
override fun convert(value: StakingActionStatusDTO): StakingActionStatus {
|
||||
return when (value) {
|
||||
StakingActionStatusDTO.CANCELED -> StakingActionStatus.CANCELED
|
||||
StakingActionStatusDTO.CREATED -> StakingActionStatus.CREATED
|
||||
StakingActionStatusDTO.WAITING_FOR_NEXT -> StakingActionStatus.WAITING_FOR_NEXT
|
||||
StakingActionStatusDTO.PROCESSING -> StakingActionStatus.PROCESSING
|
||||
StakingActionStatusDTO.FAILED -> StakingActionStatus.FAILED
|
||||
StakingActionStatusDTO.SUCCESS -> StakingActionStatus.SUCCESS
|
||||
else -> StakingActionStatus.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.data.staking.converters.action
|
||||
|
||||
import com.tangem.data.staking.converters.transaction.StakingTransactionConverter
|
||||
import com.tangem.datasource.api.stakekit.models.response.EnterActionResponse
|
||||
import com.tangem.domain.staking.model.action.StakingAction
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
class EnterActionResponseConverter(
|
||||
private val actionStatusConverter: ActionStatusConverter,
|
||||
private val stakingActionTypeConverter: StakingActionTypeConverter,
|
||||
private val transactionConverter: StakingTransactionConverter,
|
||||
) : Converter<EnterActionResponse, StakingAction> {
|
||||
|
||||
override fun convert(value: EnterActionResponse): StakingAction {
|
||||
return StakingAction(
|
||||
id = value.id,
|
||||
integrationId = value.integrationId,
|
||||
status = actionStatusConverter.convert(value.status),
|
||||
type = stakingActionTypeConverter.convert(value.type),
|
||||
currentStepIndex = value.currentStepIndex,
|
||||
amount = value.amount,
|
||||
validatorAddress = value.validatorAddress,
|
||||
validatorAddresses = value.validatorAddresses,
|
||||
transactions = value.transactions?.map(transactionConverter::convert),
|
||||
createdAt = value.createdAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.data.staking.converters.action
|
||||
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO
|
||||
import com.tangem.domain.staking.model.action.StakingActionType
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
class StakingActionTypeConverter : Converter<StakingActionTypeDTO, StakingActionType> {
|
||||
|
||||
override fun convert(value: StakingActionTypeDTO): StakingActionType {
|
||||
return when (value) {
|
||||
StakingActionTypeDTO.STAKE -> StakingActionType.STAKE
|
||||
StakingActionTypeDTO.UNSTAKE -> StakingActionType.UNSTAKE
|
||||
StakingActionTypeDTO.CLAIM_REWARDS -> StakingActionType.CLAIM_REWARDS
|
||||
StakingActionTypeDTO.RESTAKE_REWARDS -> StakingActionType.RESTAKE_REWARDS
|
||||
StakingActionTypeDTO.WITHDRAW -> StakingActionType.WITHDRAW
|
||||
StakingActionTypeDTO.RESTAKE -> StakingActionType.RESTAKE
|
||||
StakingActionTypeDTO.CLAIM_UNSTAKED -> StakingActionType.CLAIM_UNSTAKED
|
||||
StakingActionTypeDTO.UNLOCK_LOCKED -> StakingActionType.UNLOCK_LOCKED
|
||||
StakingActionTypeDTO.STAKE_LOCKED -> StakingActionType.STAKE_LOCKED
|
||||
StakingActionTypeDTO.VOTE -> StakingActionType.VOTE
|
||||
StakingActionTypeDTO.REVOKE -> StakingActionType.REVOKE
|
||||
StakingActionTypeDTO.VOTE_LOCKED -> StakingActionType.VOTE_LOCKED
|
||||
StakingActionTypeDTO.REVOTE -> StakingActionType.REVOTE
|
||||
StakingActionTypeDTO.REBOND -> StakingActionType.REBOND
|
||||
StakingActionTypeDTO.MIGRATE -> StakingActionType.MIGRATE
|
||||
StakingActionTypeDTO.UNKNOWN -> StakingActionType.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package com.tangem.data.staking.converters.error
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorMessageDTO
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse
|
||||
import com.tangem.domain.staking.model.StakingError
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class StakeKitErrorConverter(
|
||||
private val jsonAdapter: JsonAdapter<StakeKitErrorResponse>,
|
||||
) : Converter<String, StakingError> {
|
||||
|
||||
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
||||
override fun convert(value: String): StakingError {
|
||||
return try {
|
||||
val stakeKitErrorResponse = jsonAdapter.fromJson(value) ?: return StakingError.UnknownError
|
||||
|
||||
when (stakeKitErrorResponse.message) {
|
||||
StakeKitErrorMessageDTO.MINIMUM_AMOUNT_NOT_REACHED -> StakingError.MinimumAmountNotReachedError(
|
||||
amount = stakeKitErrorResponse.details?.amount ?: "",
|
||||
)
|
||||
StakeKitErrorMessageDTO.MISSING_ARGUMENTS_ERROR -> StakingError.MissingArgumentsError(
|
||||
arguments = stakeKitErrorResponse.details?.arguments ?: "",
|
||||
)
|
||||
StakeKitErrorMessageDTO.YIELD_UNDER_MAINTENANCE_ERROR -> StakingError.YieldUnderMaintenanceError(
|
||||
yieldId = stakeKitErrorResponse.details?.yieldId ?: "",
|
||||
)
|
||||
StakeKitErrorMessageDTO.INSUFFICIENT_FUNDS_ERROR ->
|
||||
StakingError.InsufficientFundsError
|
||||
StakeKitErrorMessageDTO.STAKED_POSITION_NOT_FOUND_ERROR ->
|
||||
StakingError.StakedPositionNotFoundError
|
||||
StakeKitErrorMessageDTO.INVALID_AMOUNT_SUBMITTED_ERROR ->
|
||||
StakingError.InvalidAmountSubmittedError
|
||||
StakeKitErrorMessageDTO.BALANCE_UNAVAILABLE_ERROR ->
|
||||
StakingError.BalanceUnavailableError
|
||||
StakeKitErrorMessageDTO.GAS_PRICE_UNAVAILABLE_ERROR ->
|
||||
StakingError.GasPriceUnavailableError
|
||||
StakeKitErrorMessageDTO.NOT_IMPLEMENTED_ERROR ->
|
||||
StakingError.NotImplementedError
|
||||
StakeKitErrorMessageDTO.TOKEN_NOT_FOUND_ERROR ->
|
||||
StakingError.TokenNotFoundError
|
||||
StakeKitErrorMessageDTO.BROADCAST_TRANSACTION_ERROR ->
|
||||
StakingError.BroadcastTransactionError
|
||||
StakeKitErrorMessageDTO.MISSING_GAS_PRICE_STRATEGY_ERROR ->
|
||||
StakingError.MissingGasPriceStrategyError
|
||||
StakeKitErrorMessageDTO.SUBSTRATE_MALFORMED_TRANSACTION_HASH_ERROR ->
|
||||
StakingError.SubstrateMalformedTransactionHashError
|
||||
StakeKitErrorMessageDTO.TRON_MAXIMUM_AMOUNT_OF_VALIDATORS_EXCEEDED_ERROR ->
|
||||
StakingError.TronMaximumAmountOfValidatorsExceededError
|
||||
StakeKitErrorMessageDTO.SUBSTRATE_POOL_NOT_FOUND_ERROR ->
|
||||
StakingError.SubstratePoolNotFoundError
|
||||
StakeKitErrorMessageDTO.SUBSTRATE_BONDED_AMOUNT_TOO_LOW_ERROR ->
|
||||
StakingError.SubstrateBondedAmountTooLowError
|
||||
StakeKitErrorMessageDTO.TRON_MISSING_RESOURCE_TYPE_ARGUMENT_ERROR ->
|
||||
StakingError.TronMissingResourceTypeArgumentError
|
||||
StakeKitErrorMessageDTO.AAVE_V3_POOL_FROZEN_ERROR ->
|
||||
StakingError.AaveV3PoolFrozenError
|
||||
StakeKitErrorMessageDTO.AAVE_V3_TOKEN_PAIR_NOT_FOUND_ERROR ->
|
||||
StakingError.AaveV3TokenPairNotFoundError
|
||||
StakeKitErrorMessageDTO.YEARN_VAULT_AT_MAX_CAPACITY_ERROR ->
|
||||
StakingError.YearnVaultAtMaxCapacityError
|
||||
StakeKitErrorMessageDTO.STETH_NO_WITHDRAWAL_REQUESTS_FOUND_ERROR ->
|
||||
StakingError.StETHNoWithdrawalRequestsFoundError
|
||||
StakeKitErrorMessageDTO.MORPHO_LENDING_POOL_PAUSED_ERROR ->
|
||||
StakingError.MorphoLendingPoolPausedError
|
||||
StakeKitErrorMessageDTO.NONCE_UNAVAILABLE_ERROR ->
|
||||
StakingError.NonceUnavailableError
|
||||
StakeKitErrorMessageDTO.COSMOS_ACCOUNT_NOT_FOUND_ERROR ->
|
||||
StakingError.CosmosAcccountNotFoundError
|
||||
StakeKitErrorMessageDTO.AVALANCHE_MISSING_ADDITIONAL_ADDRESSES_ARGUMENT_ERROR ->
|
||||
StakingError.AvalancheMissingAdditionalAddressesArgumentError
|
||||
StakeKitErrorMessageDTO.AVALANCHE_VALIDATOR_INFO_NOT_FOUND_ERROR ->
|
||||
StakingError.AvalancheValidatorInfoNotFoundError
|
||||
StakeKitErrorMessageDTO.SOLANA_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE_ERROR ->
|
||||
StakingError.SolanaTransactionSignatureVerificationFailureError
|
||||
StakeKitErrorMessageDTO.SOLANA_UNABLE_TO_CREATE_STAKE_ACCOUNT_ERROR ->
|
||||
StakingError.SolanaUnableTocreateStakeAccountError
|
||||
StakeKitErrorMessageDTO.SOLANA_STAKE_AMOUNT_TOO_LOW_ERROR ->
|
||||
StakingError.SolanaStakeAmountTooLowError
|
||||
StakeKitErrorMessageDTO.SOLANA_UNSTAKE_AMOUNT_TOO_LOW_ERROR ->
|
||||
StakingError.SolanaUnstakeAmountTooLowError
|
||||
StakeKitErrorMessageDTO.SOLANA_STAKE_ACCOUNTS_NOT_FOUND_ERROR ->
|
||||
StakingError.SolanaStakeAccountsNotFoundError
|
||||
StakeKitErrorMessageDTO.SOLANA_ELIGIBLE_STAKE_ACCOUNTS_NOT_FOUND_ERROR ->
|
||||
StakingError.SolanaEligibleStakeAccountsNotFoundError
|
||||
StakeKitErrorMessageDTO.TEZOS_NO_BALANCE_DELEGATED_ERROR ->
|
||||
StakingError.TezosNoBalanceDelegatedError
|
||||
StakeKitErrorMessageDTO.TEZOS_MISSING_PUBKEY_ARGUMENT_ERROR ->
|
||||
StakingError.TezosMissingPubkeyArgumentError
|
||||
StakeKitErrorMessageDTO.TEZOS_ESTIMATE_REVEAL_GAS_LIMIT_ERROR ->
|
||||
StakingError.TezosEstimateRevealGasLimitError
|
||||
StakeKitErrorMessageDTO.TEZOS_BALANCE_ALREADY_DELEGATED_ERROR ->
|
||||
StakingError.TezosBalanceAlreadyDelegatedError
|
||||
StakeKitErrorMessageDTO.BINANCE_ACCOUNT_NOT_FOUND_ERROR ->
|
||||
StakingError.BinanceAccountNotFoundError
|
||||
StakeKitErrorMessageDTO.BINANCE_MISSING_ACCOUNT_NUMBER_OR_SEQUENCE_ERROR ->
|
||||
StakingError.BinanceMissingAccountNumberOrSequenceError
|
||||
StakeKitErrorMessageDTO.GRT_STAKING_DISABLED_ERROR ->
|
||||
StakingError.GRTStakingDisabledError
|
||||
StakeKitErrorMessageDTO.GRT_STAKING_DISABLED_LEDGER_LIVE_ERROR ->
|
||||
StakingError.GRTStakingDisabledLedgerLiveError
|
||||
else -> StakingError.UnknownError
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
StakingError.UnknownError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.data.staking.converters.transaction
|
||||
|
||||
import com.tangem.data.staking.converters.TokenConverter
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingGasEstimateDTO
|
||||
import com.tangem.domain.staking.model.transaction.StakingGasEstimate
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
class GasEstimateConverter(
|
||||
private val tokenConverter: TokenConverter,
|
||||
) : Converter<StakingGasEstimateDTO, StakingGasEstimate> {
|
||||
|
||||
override fun convert(value: StakingGasEstimateDTO): StakingGasEstimate {
|
||||
return StakingGasEstimate(
|
||||
amount = value.amount,
|
||||
token = tokenConverter.convert(value.token),
|
||||
gasLimit = value.gasLimit,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.data.staking.converters.transaction
|
||||
|
||||
import com.tangem.data.staking.converters.StakingNetworkTypeConverter
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionDTO
|
||||
import com.tangem.domain.staking.model.transaction.StakingTransaction
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
class StakingTransactionConverter(
|
||||
private val networkTypeConverter: StakingNetworkTypeConverter,
|
||||
private val transactionStatusConverter: StakingTransactionStatusConverter,
|
||||
private val transactionTypeConverter: StakingTransactionTypeConverter,
|
||||
private val gasEstimateConverter: GasEstimateConverter,
|
||||
) : Converter<StakingTransactionDTO, StakingTransaction> {
|
||||
|
||||
override fun convert(value: StakingTransactionDTO): StakingTransaction {
|
||||
return StakingTransaction(
|
||||
id = value.id,
|
||||
network = networkTypeConverter.convert(value.network),
|
||||
status = transactionStatusConverter.convert(value.status),
|
||||
type = transactionTypeConverter.convert(value.type),
|
||||
hash = value.hash,
|
||||
signedTransaction = value.signedTransaction,
|
||||
unsignedTransaction = value.unsignedTransaction,
|
||||
stepIndex = value.stepIndex,
|
||||
error = value.error,
|
||||
gasEstimate = value.gasEstimate?.let { gasEstimateConverter.convert(it) },
|
||||
stakeId = value.stakeId,
|
||||
explorerUrl = value.explorerUrl,
|
||||
ledgerHwAppId = value.ledgerHwAppId,
|
||||
isMessage = value.isMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.data.staking.converters.transaction
|
||||
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionStatusDTO
|
||||
import com.tangem.domain.staking.model.transaction.StakingTransactionStatus
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
class StakingTransactionStatusConverter : Converter<StakingTransactionStatusDTO, StakingTransactionStatus> {
|
||||
|
||||
override fun convert(value: StakingTransactionStatusDTO): StakingTransactionStatus {
|
||||
return when (value) {
|
||||
StakingTransactionStatusDTO.NOT_FOUND -> StakingTransactionStatus.NOT_FOUND
|
||||
StakingTransactionStatusDTO.CREATED -> StakingTransactionStatus.CREATED
|
||||
StakingTransactionStatusDTO.BLOCKED -> StakingTransactionStatus.BLOCKED
|
||||
StakingTransactionStatusDTO.WAITING_FOR_SIGNATURE -> StakingTransactionStatus.WAITING_FOR_SIGNATURE
|
||||
StakingTransactionStatusDTO.SIGNED -> StakingTransactionStatus.SIGNED
|
||||
StakingTransactionStatusDTO.BROADCASTED -> StakingTransactionStatus.BROADCASTED
|
||||
StakingTransactionStatusDTO.PENDING -> StakingTransactionStatus.PENDING
|
||||
StakingTransactionStatusDTO.CONFIRMED -> StakingTransactionStatus.CONFIRMED
|
||||
StakingTransactionStatusDTO.FAILED -> StakingTransactionStatus.FAILED
|
||||
StakingTransactionStatusDTO.SKIPPED -> StakingTransactionStatus.SKIPPED
|
||||
StakingTransactionStatusDTO.UNKNOWN -> StakingTransactionStatus.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.data.staking.converters.transaction
|
||||
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionTypeDTO
|
||||
import com.tangem.domain.staking.model.transaction.StakingTransactionType
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
class StakingTransactionTypeConverter : Converter<StakingTransactionTypeDTO, StakingTransactionType> {
|
||||
|
||||
override fun convert(value: StakingTransactionTypeDTO): StakingTransactionType {
|
||||
return when (value) {
|
||||
StakingTransactionTypeDTO.SWAP -> StakingTransactionType.SWAP
|
||||
StakingTransactionTypeDTO.DEPOSIT -> StakingTransactionType.DEPOSIT
|
||||
StakingTransactionTypeDTO.APPROVAL -> StakingTransactionType.APPROVAL
|
||||
StakingTransactionTypeDTO.STAKE -> StakingTransactionType.STAKE
|
||||
StakingTransactionTypeDTO.CLAIM_UNSTAKED -> StakingTransactionType.CLAIM_UNSTAKED
|
||||
StakingTransactionTypeDTO.CLAIM_REWARDS -> StakingTransactionType.CLAIM_REWARDS
|
||||
StakingTransactionTypeDTO.RESTAKE_REWARDS -> StakingTransactionType.RESTAKE_REWARDS
|
||||
StakingTransactionTypeDTO.UNSTAKE -> StakingTransactionType.UNSTAKE
|
||||
StakingTransactionTypeDTO.SPLIT -> StakingTransactionType.SPLIT
|
||||
StakingTransactionTypeDTO.MERGE -> StakingTransactionType.MERGE
|
||||
StakingTransactionTypeDTO.LOCK -> StakingTransactionType.LOCK
|
||||
StakingTransactionTypeDTO.UNLOCK -> StakingTransactionType.UNLOCK
|
||||
StakingTransactionTypeDTO.SUPPLY -> StakingTransactionType.SUPPLY
|
||||
StakingTransactionTypeDTO.BRIDGE -> StakingTransactionType.BRIDGE
|
||||
StakingTransactionTypeDTO.VOTE -> StakingTransactionType.VOTE
|
||||
StakingTransactionTypeDTO.REVOKE -> StakingTransactionType.REVOKE
|
||||
StakingTransactionTypeDTO.RESTAKE -> StakingTransactionType.RESTAKE
|
||||
StakingTransactionTypeDTO.REBOND -> StakingTransactionType.REBOND
|
||||
StakingTransactionTypeDTO.WITHDRAW -> StakingTransactionType.WITHDRAW
|
||||
StakingTransactionTypeDTO.CREATE_ACCOUNT -> StakingTransactionType.CREATE_ACCOUNT
|
||||
StakingTransactionTypeDTO.REVEAL -> StakingTransactionType.REVEAL
|
||||
StakingTransactionTypeDTO.MIGRATE -> StakingTransactionType.MIGRATE
|
||||
StakingTransactionTypeDTO.UTXO_P_TO_C_IMPORT -> StakingTransactionType.UTXO_P_TO_C_IMPORT
|
||||
StakingTransactionTypeDTO.UTXO_C_TO_P_IMPORT -> StakingTransactionType.UTXO_C_TO_P_IMPORT
|
||||
StakingTransactionTypeDTO.UNFREEZE_LEGACY -> StakingTransactionType.UNFREEZE_LEGACY
|
||||
StakingTransactionTypeDTO.UNFREEZE_LEGACY_BANDWIDTH -> StakingTransactionType.UNFREEZE_LEGACY_BANDWIDTH
|
||||
StakingTransactionTypeDTO.UNFREEZE_LEGACY_ENERGY -> StakingTransactionType.UNFREEZE_LEGACY_ENERGY
|
||||
StakingTransactionTypeDTO.UNFREEZE_BANDWIDTH -> StakingTransactionType.UNFREEZE_BANDWIDTH
|
||||
StakingTransactionTypeDTO.UNFREEZE_ENERGY -> StakingTransactionType.UNFREEZE_ENERGY
|
||||
StakingTransactionTypeDTO.FREEZE_BANDWIDTH -> StakingTransactionType.FREEZE_BANDWIDTH
|
||||
StakingTransactionTypeDTO.FREEZE_ENERGY -> StakingTransactionType.FREEZE_ENERGY
|
||||
StakingTransactionTypeDTO.UNDELEGATE_BANDWIDTH -> StakingTransactionType.UNDELEGATE_BANDWIDTH
|
||||
StakingTransactionTypeDTO.UNDELEGATE_ENERGY -> StakingTransactionType.UNDELEGATE_ENERGY
|
||||
StakingTransactionTypeDTO.P2P_NODE_REQUEST -> StakingTransactionType.P2P_NODE_REQUEST
|
||||
StakingTransactionTypeDTO.LUGANODES_PROVISION -> StakingTransactionType.LUGANODES_PROVISION
|
||||
StakingTransactionTypeDTO.LUGANODES_EXIT_REQUEST -> StakingTransactionType.LUGANODES_EXIT_REQUEST
|
||||
StakingTransactionTypeDTO.INFSTONES_PROVISION -> StakingTransactionType.INFSTONES_PROVISION
|
||||
StakingTransactionTypeDTO.INFSTONES_EXIT_REQUEST -> StakingTransactionType.INFSTONES_EXIT_REQUEST
|
||||
StakingTransactionTypeDTO.INFSTONES_CLAIM_REQUEST -> StakingTransactionType.INFSTONES_CLAIM_REQUEST
|
||||
StakingTransactionTypeDTO.UNKNOWN -> StakingTransactionType.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,17 @@
|
|||
package com.tangem.data.staking.di
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.staking.DefaultStakingErrorResolver
|
||||
import com.tangem.data.staking.DefaultStakingRepository
|
||||
import com.tangem.data.staking.converters.error.StakeKitErrorConverter
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.token.StakingBalanceStore
|
||||
import com.tangem.datasource.local.token.StakingYieldsStore
|
||||
import com.tangem.domain.staking.repositories.StakingErrorResolver
|
||||
import com.tangem.domain.staking.repositories.StakingRepository
|
||||
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -19,13 +29,30 @@ internal object StakingDataModule {
|
|||
@Singleton
|
||||
fun provideStakingRepository(
|
||||
stakeKitApi: StakeKitApi,
|
||||
stakingFeatureToggles: StakingFeatureToggles,
|
||||
coroutineDispatcherProvider: CoroutineDispatcherProvider,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
stakingTokenStore: StakingYieldsStore,
|
||||
stakingBalanceStore: StakingBalanceStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
stakingFeatureToggle: StakingFeatureToggles,
|
||||
cacheRegistry: CacheRegistry,
|
||||
): StakingRepository {
|
||||
return DefaultStakingRepository(
|
||||
stakeKitApi = stakeKitApi,
|
||||
stakingFeatureToggles = stakingFeatureToggles,
|
||||
dispatchers = coroutineDispatcherProvider,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
stakingYieldsStore = stakingTokenStore,
|
||||
stakingBalanceStore = stakingBalanceStore,
|
||||
dispatchers = dispatchers,
|
||||
cacheRegistry = cacheRegistry,
|
||||
stakingFeatureToggle = stakingFeatureToggle,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
internal fun provideStakingErrorResolver(@NetworkMoshi moshi: Moshi): StakingErrorResolver {
|
||||
val jsonAdapter = moshi.adapter(StakeKitErrorResponse::class.java)
|
||||
return DefaultStakingErrorResolver(
|
||||
stakeKitErrorConverter = StakeKitErrorConverter(jsonAdapter),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
|
|||
import com.tangem.datasource.local.network.NetworksStatusesStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.quote.QuotesStore
|
||||
import com.tangem.datasource.local.token.AssetsStore
|
||||
import com.tangem.datasource.local.token.ExpressAssetsStore
|
||||
import com.tangem.datasource.local.token.UserTokensStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.tokens.repository.*
|
||||
|
|
@ -31,7 +31,7 @@ internal object TokensDataModule {
|
|||
userTokensStore: UserTokensStore,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
assetsStore: AssetsStore,
|
||||
expressAssetsStore: ExpressAssetsStore,
|
||||
cacheRegistry: CacheRegistry,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): CurrenciesRepository {
|
||||
|
|
@ -41,7 +41,7 @@ internal object TokensDataModule {
|
|||
userTokensStore = userTokensStore,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
userWalletsStore = userWalletsStore,
|
||||
assetsStore = assetsStore,
|
||||
expressAssetsStore = expressAssetsStore,
|
||||
cacheRegistry = cacheRegistry,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
|
@ -88,10 +88,10 @@ internal object TokensDataModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideDefaultMarketCoinsRepository(
|
||||
assetsStore: AssetsStore,
|
||||
expressAssetsStore: ExpressAssetsStore,
|
||||
coroutineDispatcherProvider: CoroutineDispatcherProvider,
|
||||
): MarketCryptoCurrencyRepository {
|
||||
return DefaultMarketCryptoCurrencyRepository(assetsStore, coroutineDispatcherProvider)
|
||||
return DefaultMarketCryptoCurrencyRepository(expressAssetsStore, coroutineDispatcherProvider)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import com.tangem.datasource.api.express.models.request.AssetsRequestBody
|
|||
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.local.token.AssetsStore
|
||||
import com.tangem.datasource.local.token.ExpressAssetsStore
|
||||
import com.tangem.datasource.local.token.UserTokensStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
|
|
@ -46,7 +46,7 @@ internal class DefaultCurrenciesRepository(
|
|||
private val userTokensStore: UserTokensStore,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val assetsStore: AssetsStore,
|
||||
private val expressAssetsStore: ExpressAssetsStore,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : CurrenciesRepository {
|
||||
|
|
@ -61,6 +61,18 @@ internal class DefaultCurrenciesRepository(
|
|||
private val isMultiCurrencyWalletCurrenciesFetching = MutableStateFlow(
|
||||
value = emptyMap<UserWalletId, Boolean>(),
|
||||
)
|
||||
private val parallelTransactionsEnabledBlockchains = setOf(
|
||||
Blockchain.Ethereum,
|
||||
Blockchain.EthereumTestnet,
|
||||
Blockchain.Polygon,
|
||||
Blockchain.PolygonTestnet,
|
||||
Blockchain.Arbitrum,
|
||||
Blockchain.ArbitrumTestnet,
|
||||
Blockchain.Binance,
|
||||
Blockchain.BinanceTestnet,
|
||||
Blockchain.Tron,
|
||||
Blockchain.TronTestnet,
|
||||
)
|
||||
|
||||
override suspend fun saveTokens(
|
||||
userWalletId: UserWalletId,
|
||||
|
|
@ -249,7 +261,7 @@ internal class DefaultCurrenciesRepository(
|
|||
|
||||
launch(dispatchers.io) {
|
||||
combine(
|
||||
getMultiCurrencyWalletCurrencies(userWallet).distinctUntilChanged(),
|
||||
getMultiCurrencyWalletCurrencies(userWallet),
|
||||
isMultiCurrencyWalletCurrenciesFetching.map { it.getOrElse(userWallet.walletId) { false } },
|
||||
) { currencies, isFetching ->
|
||||
send(currencies, isStillLoading = isFetching)
|
||||
|
|
@ -368,18 +380,19 @@ internal class DefaultCurrenciesRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override fun hasPendingTransactions(
|
||||
override fun isSendBlockedByPendingTransactions(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
coinStatus: CryptoCurrencyStatus?,
|
||||
): Boolean {
|
||||
val blockchain = Blockchain.fromId(cryptoCurrencyStatus.currency.network.id.value)
|
||||
val isBitcoinBlockchain = blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet
|
||||
|
||||
return if (cryptoCurrencyStatus.currency is CryptoCurrency.Coin && isBitcoinBlockchain) {
|
||||
val outgoingTransactions = cryptoCurrencyStatus.value.pendingTransactions.filter { it.isOutgoing }
|
||||
outgoingTransactions.isNotEmpty()
|
||||
} else {
|
||||
coinStatus?.value?.hasCurrentNetworkTransactions == true
|
||||
return when {
|
||||
cryptoCurrencyStatus.currency is CryptoCurrency.Coin && isBitcoinBlockchain -> {
|
||||
val outgoingTransactions = cryptoCurrencyStatus.value.pendingTransactions.filter { it.isOutgoing }
|
||||
outgoingTransactions.isNotEmpty()
|
||||
}
|
||||
parallelTransactionsEnabledBlockchains.contains(blockchain) -> false
|
||||
else -> coinStatus?.value?.hasCurrentNetworkTransactions == true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -429,21 +442,23 @@ internal class DefaultCurrenciesRepository(
|
|||
}
|
||||
|
||||
private suspend fun fetchTokensIfCacheExpired(userWallet: UserWallet, refresh: Boolean) {
|
||||
try {
|
||||
isMultiCurrencyWalletCurrenciesFetching.update {
|
||||
it + (userWallet.walletId to true)
|
||||
}
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getTokensCacheKey(userWallet.walletId),
|
||||
skipCache = refresh,
|
||||
block = {
|
||||
isMultiCurrencyWalletCurrenciesFetching.update {
|
||||
it + (userWallet.walletId to true)
|
||||
}
|
||||
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getTokensCacheKey(userWallet.walletId),
|
||||
skipCache = refresh,
|
||||
block = { fetchTokens(userWallet) },
|
||||
)
|
||||
} finally {
|
||||
isMultiCurrencyWalletCurrenciesFetching.update {
|
||||
it - userWallet.walletId
|
||||
}
|
||||
}
|
||||
try {
|
||||
fetchTokens(userWallet)
|
||||
} finally {
|
||||
isMultiCurrencyWalletCurrenciesFetching.update {
|
||||
it - userWallet.walletId
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun fetchTokens(userWallet: UserWallet) {
|
||||
|
|
@ -497,7 +512,7 @@ internal class DefaultCurrenciesRepository(
|
|||
),
|
||||
)
|
||||
|
||||
assetsStore.store(userWalletId, response.getOrThrow())
|
||||
expressAssetsStore.store(userWalletId, response.getOrThrow())
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
|
||||
import com.tangem.datasource.local.token.AssetsStore
|
||||
import com.tangem.datasource.local.token.ExpressAssetsStore
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
|
@ -9,7 +9,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import kotlinx.coroutines.withContext
|
||||
|
||||
class DefaultMarketCryptoCurrencyRepository(
|
||||
private val assetsStore: AssetsStore,
|
||||
private val expressAssetsStore: ExpressAssetsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MarketCryptoCurrencyRepository {
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ class DefaultMarketCryptoCurrencyRepository(
|
|||
val contractAddress =
|
||||
(cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE
|
||||
|
||||
val asset = assetsStore.getSyncOrNull(userWalletId)?.find {
|
||||
val asset = expressAssetsStore.getSyncOrNull(userWalletId)?.find {
|
||||
it.network == cryptoCurrency.network.backendId &&
|
||||
it.contractAddress.equals(contractAddress, ignoreCase = true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,29 +124,81 @@ internal class DefaultNetworksRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getNetworkAddress(
|
||||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
): CryptoCurrencyAddress = withContext(dispatchers.io) {
|
||||
CryptoCurrencyAddress(
|
||||
cryptoCurrency = currency,
|
||||
address = walletManagersFacade.getAddresses(userWalletId, currency.network)
|
||||
.firstOrNull { it.type == AddressType.Default }
|
||||
?.value.orEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun getNetworkAddressFlow(
|
||||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
): Flow<CryptoCurrencyAddress> = channelFlow {
|
||||
launch(dispatchers.io) {
|
||||
send(getNetworkAddress(userWalletId, currency))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getNetworkAddresses(userWalletId: UserWalletId): List<CryptoCurrencyAddress> =
|
||||
withContext(dispatchers.io) {
|
||||
// Get list of currencies matching [network]
|
||||
val currencies = getCurrencies(userWalletId)
|
||||
|
||||
// There is no currencies matching given [networks] in [userWalletId]
|
||||
if (currencies.toList().isEmpty()) return@withContext emptyList()
|
||||
|
||||
currencies.toList().map { currency ->
|
||||
CryptoCurrencyAddress(
|
||||
cryptoCurrency = currency,
|
||||
address = walletManagersFacade.getAddresses(userWalletId, currency.network)
|
||||
.firstOrNull { it.type == AddressType.Default }
|
||||
?.value.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNetworkAddressesFlow(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): Flow<List<CryptoCurrencyAddress>> = channelFlow {
|
||||
launch(dispatchers.io) {
|
||||
send(getNetworkAddresses(userWalletId, network))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow<List<CryptoCurrencyAddress>> = channelFlow {
|
||||
launch(dispatchers.io) {
|
||||
send(getNetworkAddresses(userWalletId))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworksStatusesIfCacheExpired(
|
||||
userWalletId: UserWalletId,
|
||||
networks: Set<Network>,
|
||||
refresh: Boolean,
|
||||
) {
|
||||
try {
|
||||
isNetworkStatusesFetching.update {
|
||||
it + (userWalletId to true)
|
||||
}
|
||||
val currencies = getCurrencies(userWalletId, networks)
|
||||
val networksDeferred = networks.mapNotNull { network ->
|
||||
fetchNetworkStatusIfCacheExpired(userWalletId, network, currencies, refresh)
|
||||
}
|
||||
|
||||
val currencies = getCurrencies(userWalletId, networks)
|
||||
coroutineScope {
|
||||
networks
|
||||
.map { network ->
|
||||
async {
|
||||
fetchNetworkStatusIfCacheExpired(userWalletId, network, currencies, refresh)
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
} finally {
|
||||
isNetworkStatusesFetching.update {
|
||||
it - userWalletId
|
||||
if (networksDeferred.isNotEmpty()) {
|
||||
try {
|
||||
isNetworkStatusesFetching.update {
|
||||
it + (userWalletId to true)
|
||||
}
|
||||
|
||||
networksDeferred.awaitAll()
|
||||
} finally {
|
||||
isNetworkStatusesFetching.update {
|
||||
it - userWalletId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -172,12 +224,19 @@ internal class DefaultNetworksRepository(
|
|||
network: Network,
|
||||
currencies: Sequence<CryptoCurrency>,
|
||||
refresh: Boolean,
|
||||
) {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getNetworksStatusesCacheKey(userWalletId, network),
|
||||
skipCache = refresh,
|
||||
block = { fetchNetworkStatus(userWalletId, network, currencies) },
|
||||
)
|
||||
): Deferred<Unit>? = coroutineScope {
|
||||
val key = getNetworksStatusesCacheKey(userWalletId, network)
|
||||
if (refresh || cacheRegistry.isExpired(key)) {
|
||||
async {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = key,
|
||||
skipCache = refresh,
|
||||
block = { fetchNetworkStatus(userWalletId, network, currencies) },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworkStatus(
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ internal class QuotesConverter : Converter<StoredQuote, Quote> {
|
|||
return Quote(
|
||||
rawCurrencyId = rawCurrencyId,
|
||||
fiatRate = responseQuote.price ?: BigDecimal.ZERO,
|
||||
priceChange = (responseQuote.priceChange ?: BigDecimal.ZERO).movePointLeft(2),
|
||||
priceChange = (responseQuote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ dependencies {
|
|||
|
||||
/** Tangem SDKs */
|
||||
implementation(deps.tangem.blockchain)
|
||||
implementation(deps.tangem.card.core)
|
||||
|
||||
/** Core */
|
||||
implementation(projects.core.datasource)
|
||||
|
|
@ -25,6 +26,7 @@ dependencies {
|
|||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.transaction.models)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -4,22 +4,29 @@ import androidx.core.text.isDigitsOnly
|
|||
import com.tangem.blockchain.blockchains.algorand.AlgorandTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.hedera.HederaTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.stellar.StellarMemo
|
||||
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.ton.TonTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.tron.TronTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.datasource.local.walletmanager.WalletManagersStore
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.transaction.TransactionRepository
|
||||
import com.tangem.domain.transaction.models.TransactionType
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
import com.tangem.blockchain.blockchains.tron.TransactionType as SdkTransactionType
|
||||
|
||||
internal class DefaultTransactionRepository(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
|
|
@ -37,7 +44,7 @@ internal class DefaultTransactionRepository(
|
|||
isSwap: Boolean,
|
||||
txExtras: TransactionExtras?,
|
||||
hash: String?,
|
||||
): TransactionData? = withContext(coroutineDispatcherProvider.io) {
|
||||
): TransactionData.Uncompiled? = withContext(coroutineDispatcherProvider.io) {
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
|
|
@ -45,7 +52,7 @@ internal class DefaultTransactionRepository(
|
|||
derivationPath = network.derivationPath.value,
|
||||
)
|
||||
|
||||
return@withContext walletManager?.createTransactionInternal(
|
||||
return@withContext walletManager?.createTransactionDataInternal(
|
||||
amount = amount,
|
||||
fee = fee,
|
||||
memo = memo,
|
||||
|
|
@ -78,7 +85,7 @@ internal class DefaultTransactionRepository(
|
|||
val validator = walletManager as? TransactionValidator
|
||||
|
||||
if (validator != null) {
|
||||
val transaction = walletManager.createTransactionInternal(
|
||||
val transactionData = walletManager.createTransactionDataInternal(
|
||||
amount = amount,
|
||||
fee = fee ?: Fee.Common(amount = amount),
|
||||
memo = memo,
|
||||
|
|
@ -89,7 +96,7 @@ internal class DefaultTransactionRepository(
|
|||
hash = hash,
|
||||
)
|
||||
|
||||
validator.validate(transaction = transaction)
|
||||
validator.validate(transactionData = transactionData)
|
||||
} else {
|
||||
Timber.e("${walletManager?.wallet?.blockchain} does not support transaction validation")
|
||||
Result.success(Unit)
|
||||
|
|
@ -111,8 +118,41 @@ internal class DefaultTransactionRepository(
|
|||
(walletManager as TransactionSender).send(txData, signer)
|
||||
}
|
||||
|
||||
override fun createTransactionDataExtras(
|
||||
data: String,
|
||||
network: Network,
|
||||
transactionType: TransactionType,
|
||||
nonce: BigInteger?,
|
||||
gasLimit: BigInteger?,
|
||||
): TransactionExtras {
|
||||
val blockchain = Blockchain.fromNetworkId(networkId = network.backendId)
|
||||
?: error("Blockchain not found")
|
||||
return when {
|
||||
blockchain.isEvm() -> {
|
||||
EthereumTransactionExtras(
|
||||
data = data.hexToBytes(),
|
||||
gasLimit = gasLimit,
|
||||
nonce = nonce,
|
||||
)
|
||||
}
|
||||
blockchain == Blockchain.Tron -> {
|
||||
TronTransactionExtras(
|
||||
data = data.hexToBytes(),
|
||||
txType = convertToSdkTransactionType(transactionType),
|
||||
)
|
||||
}
|
||||
else -> error("Data extras not supported for $blockchain")
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertToSdkTransactionType(transactionType: TransactionType): SdkTransactionType {
|
||||
return when (transactionType) {
|
||||
TransactionType.APPROVE -> SdkTransactionType.APPROVE
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun WalletManager.createTransactionInternal(
|
||||
private fun WalletManager.createTransactionDataInternal(
|
||||
amount: Amount,
|
||||
fee: Fee,
|
||||
memo: String?,
|
||||
|
|
@ -121,7 +161,7 @@ internal class DefaultTransactionRepository(
|
|||
isSwap: Boolean,
|
||||
txExtras: TransactionExtras?,
|
||||
hash: String?,
|
||||
): TransactionData {
|
||||
): TransactionData.Uncompiled {
|
||||
// TODO: refactor workaround to use general mechanism in bsdk for build tx for DEX
|
||||
val txAmount = if (isSwap) {
|
||||
createAmountForSwap(amount)
|
||||
|
|
|
|||
|
|
@ -10,10 +10,8 @@ import com.tangem.common.card.EllipticCurve
|
|||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.visa.utils.VisaConfig
|
||||
import com.tangem.data.visa.utils.VisaCurrencyFactory
|
||||
import com.tangem.data.visa.utils.VisaTxDetailsFactory
|
||||
import com.tangem.data.visa.utils.VisaTxHistoryPagingSource
|
||||
import com.tangem.data.visa.config.VisaLibLoader
|
||||
import com.tangem.data.visa.utils.*
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
|
|
@ -24,8 +22,6 @@ import com.tangem.domain.visa.model.VisaTxHistoryItem
|
|||
import com.tangem.domain.visa.repository.VisaRepository
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.lib.visa.VisaContractInfoProvider
|
||||
import com.tangem.lib.visa.api.VisaApi
|
||||
import com.tangem.lib.visa.model.VisaTxHistoryResponse
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -35,9 +31,8 @@ import kotlinx.coroutines.withContext
|
|||
import java.math.BigDecimal
|
||||
|
||||
internal class DefaultVisaRepository(
|
||||
private val visaContractInfoProvider: VisaContractInfoProvider,
|
||||
private val visaLibLoader: VisaLibLoader,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val visaApi: VisaApi,
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -76,9 +71,11 @@ internal class DefaultVisaRepository(
|
|||
}
|
||||
|
||||
private suspend fun fetchVisaCurrency(address: String) {
|
||||
val contractInfoProvider = visaLibLoader.getOrCreateProvider()
|
||||
|
||||
parZip(
|
||||
dispatchers.io,
|
||||
{ visaContractInfoProvider.getContractInfo(address) },
|
||||
{ contractInfoProvider.getContractInfo(address) },
|
||||
{ getFiatRate() },
|
||||
{ contractInfo, fiatRate ->
|
||||
fetchedCurrencies.update { value ->
|
||||
|
|
@ -97,6 +94,7 @@ internal class DefaultVisaRepository(
|
|||
): Flow<PagingData<VisaTxHistoryItem>> {
|
||||
val userWallet = findVisaUserWallet(userWalletId)
|
||||
val cardPubKey = getCardPubKey(userWallet)
|
||||
val api = visaLibLoader.getOrCreateApi()
|
||||
val pager = Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = pageSize,
|
||||
|
|
@ -110,7 +108,7 @@ internal class DefaultVisaRepository(
|
|||
isRefresh = isRefresh,
|
||||
),
|
||||
cacheRegistry = cacheRegistry,
|
||||
visaApi = visaApi,
|
||||
visaApi = api,
|
||||
fetchedItems = fetchedHistoryItems,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
|
@ -137,7 +135,7 @@ internal class DefaultVisaRepository(
|
|||
}
|
||||
|
||||
private suspend fun makeAddress(userWalletId: UserWalletId): String {
|
||||
if (IS_DEMO_MODE_ENABLED) return DEMO_ADDRESS
|
||||
if (VisaConstants.IS_DEMO_MODE_ENABLED) return getDemoAddress()
|
||||
|
||||
val userWallet = findVisaUserWallet(userWalletId)
|
||||
val walletAddresses = makeWalletAddresses(userWallet)
|
||||
|
|
@ -149,13 +147,13 @@ internal class DefaultVisaRepository(
|
|||
}
|
||||
|
||||
private suspend fun getFiatRate(): BigDecimal? {
|
||||
val fiatCurrencyId = VisaConfig.fiatCurrency.code.lowercase()
|
||||
val fiatCurrencyId = VisaConstants.fiatCurrency.code.lowercase()
|
||||
val quotes = tangemTechApi.getQuotes(
|
||||
currencyId = fiatCurrencyId,
|
||||
coinIds = VisaConfig.TOKEN_ID,
|
||||
coinIds = VisaConstants.TOKEN_ID,
|
||||
).getOrThrow()
|
||||
|
||||
return quotes.quotes[VisaConfig.TOKEN_ID]?.price
|
||||
return quotes.quotes[VisaConstants.TOKEN_ID]?.price
|
||||
}
|
||||
|
||||
private fun makeWalletAddresses(userWallet: UserWallet): Set<Address> {
|
||||
|
|
@ -165,7 +163,7 @@ internal class DefaultVisaRepository(
|
|||
}
|
||||
|
||||
private fun getCardPubKey(userWallet: UserWallet): String {
|
||||
if (IS_DEMO_MODE_ENABLED) return DEMO_PUBLIC_KEY
|
||||
if (VisaConstants.IS_DEMO_MODE_ENABLED) return getDemoPublicKey()
|
||||
|
||||
val cardWallet = userWallet.scanResponse.card.wallets.firstOrNull {
|
||||
it.curve == EllipticCurve.Secp256k1
|
||||
|
|
@ -189,12 +187,4 @@ internal class DefaultVisaRepository(
|
|||
private fun getVisaCurrencyKey(address: String): String {
|
||||
return "visa_currency_$address"
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// Must be `false` in production
|
||||
const val IS_DEMO_MODE_ENABLED = false
|
||||
|
||||
const val DEMO_ADDRESS = "0x40d8194b7168723ece51fa34d16825c60ba03dfa"
|
||||
const val DEMO_PUBLIC_KEY = "02C2BBA0DA1E066EA968C1EB129499F6DEBC5FD82D70D61DCAF691CDB69AF5D8B9"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.data.visa.config
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
|
||||
internal data class VisaConfig(
|
||||
@Json(name = "testnet")
|
||||
val testnet: Addresses,
|
||||
@Json(name = "mainnet")
|
||||
val mainnet: Addresses,
|
||||
@Json(name = "txHistoryAPIAdditionalHeaders")
|
||||
val header: Header,
|
||||
) {
|
||||
|
||||
data class Addresses(
|
||||
@Json(name = "paymentAccountRegistry")
|
||||
val paymentAccountRegistry: String,
|
||||
@Json(name = "bridgeProcessor")
|
||||
val bridgeProcessor: String,
|
||||
)
|
||||
|
||||
data class Header(
|
||||
@Json(name = "x-asn")
|
||||
val xAsn: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.tangem.data.visa.config
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.visa.BuildConfig
|
||||
import com.tangem.data.visa.utils.VisaConstants
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.lib.visa.VisaContractInfoProvider
|
||||
import com.tangem.lib.visa.api.VisaApi
|
||||
import com.tangem.lib.visa.api.VisaApiBuilder
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class VisaLibLoader @Inject constructor(
|
||||
private val assetLoader: AssetLoader,
|
||||
@NetworkMoshi private val moshi: Moshi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
private val createMutex = Mutex()
|
||||
|
||||
private var config: VisaConfig? = null
|
||||
|
||||
private var provider: VisaContractInfoProvider? = null
|
||||
private var api: VisaApi? = null
|
||||
|
||||
suspend fun getOrCreateProvider(): VisaContractInfoProvider = provider ?: createProvider()
|
||||
|
||||
suspend fun getOrCreateApi(): VisaApi = api ?: createApi()
|
||||
|
||||
private suspend fun createProvider(): VisaContractInfoProvider = createMutex.withLock {
|
||||
val config = getOrLoadConfig()
|
||||
|
||||
provider = VisaContractInfoProvider.Builder(
|
||||
useTestnetRpc = VisaConstants.USE_TEST_ENV,
|
||||
bridgeProcessorAddress = if (VisaConstants.USE_TEST_ENV) {
|
||||
config.testnet.bridgeProcessor
|
||||
} else {
|
||||
config.mainnet.bridgeProcessor
|
||||
},
|
||||
paymentAccountRegistryAddress = if (VisaConstants.USE_TEST_ENV) {
|
||||
config.testnet.paymentAccountRegistry
|
||||
} else {
|
||||
config.mainnet.paymentAccountRegistry
|
||||
},
|
||||
isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED,
|
||||
dispatchers = dispatchers,
|
||||
).build()
|
||||
|
||||
return requireNotNull(provider) {
|
||||
"Visa provider is not created"
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createApi(): VisaApi = createMutex.withLock {
|
||||
val config = getOrLoadConfig()
|
||||
|
||||
api = VisaApiBuilder(
|
||||
useDevApi = VisaConstants.USE_TEST_ENV,
|
||||
isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED,
|
||||
moshi = moshi,
|
||||
headers = mapOf(
|
||||
X_ASN_HEADER_NAME to config.header.xAsn,
|
||||
),
|
||||
).build()
|
||||
|
||||
return requireNotNull(api) {
|
||||
"Visa API is not created"
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getOrLoadConfig(): VisaConfig {
|
||||
config = assetLoader.load<VisaConfig>(VISA_CONFIG_FILE_NAME)
|
||||
|
||||
return requireNotNull(config) {
|
||||
"Visa config is not found"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val VISA_CONFIG_FILE_NAME = "tangem-app-config/visa_config"
|
||||
private const val X_ASN_HEADER_NAME = "x-asn"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,11 @@
|
|||
package com.tangem.data.visa.di
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.visa.BuildConfig
|
||||
import com.tangem.data.visa.DefaultVisaRepository
|
||||
import com.tangem.data.visa.config.VisaLibLoader
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.visa.repository.VisaRepository
|
||||
import com.tangem.lib.visa.VisaContractInfoProvider
|
||||
import com.tangem.lib.visa.api.VisaApiBuilder
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -25,29 +21,16 @@ internal object ImplementedVisaDataModule {
|
|||
@Singleton
|
||||
@ImplementedVisaRepository
|
||||
fun provideVisaRepository(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
visaLibLoader: VisaLibLoader,
|
||||
tangemTechApi: TangemTechApi,
|
||||
cacheRegistry: CacheRegistry,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): VisaRepository {
|
||||
val contractInfoProvider = VisaContractInfoProvider.Builder(
|
||||
isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED,
|
||||
dispatchers = dispatchers,
|
||||
).build()
|
||||
val visaApi = VisaApiBuilder(
|
||||
useDevApi = true,
|
||||
isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED,
|
||||
moshi = moshi,
|
||||
).build()
|
||||
|
||||
return DefaultVisaRepository(
|
||||
contractInfoProvider,
|
||||
tangemTechApi,
|
||||
visaApi,
|
||||
cacheRegistry,
|
||||
userWalletsStore,
|
||||
dispatchers,
|
||||
)
|
||||
}
|
||||
): VisaRepository = DefaultVisaRepository(
|
||||
visaLibLoader,
|
||||
tangemTechApi,
|
||||
cacheRegistry,
|
||||
userWalletsStore,
|
||||
dispatchers,
|
||||
)
|
||||
}
|
||||
|
|
@ -7,9 +7,9 @@ import java.util.Currency
|
|||
internal fun findCurrencyByNumericCode(code: Int): Currency {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
Currency.getAvailableCurrencies().firstOrNull { it.numericCode == code }
|
||||
?: Currency.getInstance(VisaConfig.fiatCurrency.code)
|
||||
?: Currency.getInstance(VisaConstants.fiatCurrency.code)
|
||||
} else {
|
||||
Timber.w("Unable to get currency by numeric code on API level ${Build.VERSION.SDK_INT}")
|
||||
Currency.getInstance(VisaConfig.fiatCurrency.code)
|
||||
Currency.getInstance(VisaConstants.fiatCurrency.code)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
|
||||
internal object VisaConfig {
|
||||
|
||||
const val NETWORK_NAME = "Polygon PoS"
|
||||
|
||||
const val TOKEN_ID = "tether"
|
||||
|
||||
val fiatCurrency = AppCurrency(
|
||||
code = "EUR",
|
||||
name = "Euro",
|
||||
symbol = "€",
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.data.visa.utils
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
|
||||
internal object VisaConstants {
|
||||
|
||||
const val NETWORK_NAME = "Polygon PoS"
|
||||
|
||||
const val TOKEN_ID = "tether"
|
||||
|
||||
val fiatCurrency = AppCurrency(
|
||||
code = "EUR",
|
||||
name = "Euro",
|
||||
symbol = "€",
|
||||
)
|
||||
|
||||
/*
|
||||
* Must be `false` in production
|
||||
* Don't forget to change CardTypesResolver.isVisaWallet
|
||||
* */
|
||||
const val IS_DEMO_MODE_ENABLED = false
|
||||
|
||||
const val USE_TEST_ENV = true
|
||||
|
||||
const val DEMO_TESTNET_ADDRESS = "0x51d034eb1563d0d2e66379ef37756d3c14936c44"
|
||||
const val DEMO_TESTNET_PUBLIC_KEY = "03FA1122B809079F79C4E0F657FE11337FEC88C3FB3C6341B2CE2E4F5D9241DD86"
|
||||
|
||||
const val DEMO_MAINNET_ADDRESS = "0x927e3ef2b3d85bacf9e520379f64f6627d323fcd"
|
||||
const val DEMO_MAINNET_PUBLIC_KEY = "02AC61CD57B8011BEE8BB489FB744845CC113AD379132C56015EE70528B6A88E92"
|
||||
}
|
||||
|
||||
internal fun getDemoAddress(): String {
|
||||
return if (VisaConstants.USE_TEST_ENV) {
|
||||
VisaConstants.DEMO_TESTNET_ADDRESS
|
||||
} else {
|
||||
VisaConstants.DEMO_MAINNET_ADDRESS
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getDemoPublicKey(): String {
|
||||
return if (VisaConstants.USE_TEST_ENV) {
|
||||
VisaConstants.DEMO_TESTNET_PUBLIC_KEY
|
||||
} else VisaConstants.DEMO_MAINNET_PUBLIC_KEY
|
||||
}
|
||||
|
|
@ -21,10 +21,10 @@ internal class VisaCurrencyFactory {
|
|||
|
||||
return VisaCurrency(
|
||||
symbol = contractInfo.token.symbol,
|
||||
networkName = VisaConfig.NETWORK_NAME,
|
||||
networkName = VisaConstants.NETWORK_NAME,
|
||||
decimals = contractInfo.token.decimals,
|
||||
fiatRate = fiatRate,
|
||||
fiatCurrency = VisaConfig.fiatCurrency,
|
||||
fiatCurrency = VisaConstants.fiatCurrency,
|
||||
balances = with(contractInfo) {
|
||||
VisaCurrency.Balances(
|
||||
total = balances.total,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue