Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-07 15:08:26 +03:00
commit 2a5e3fa7eb
220 changed files with 5932 additions and 1815 deletions

View file

@ -24,7 +24,12 @@ internal object BlockchainInfoConverter : Converter<WalletManager, BlockchainInf
addresses = value.wallet.mapAddresses(Address::value),
explorerLinks = value.wallet.mapAddresses { value.wallet.getExploreUrl(it.value) },
tokens = value.cardTokens.map { token ->
BlockchainInfo.TokenInfo(id = token.id, name = token.name, contractAddress = token.contractAddress)
BlockchainInfo.TokenInfo(
id = token.id,
name = token.name,
contractAddress = token.contractAddress,
decimals = token.decimals.toString(),
)
},
)
}

View file

@ -20,6 +20,10 @@ internal object CardInfoConverter : Converter<ScanResponse, CardInfo> {
CardInfo(
userWalletId = createUserWalletId(scanResponse = value),
cardId = card.cardId,
cardsCount = when (val status = value.card.backupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount.toString()
else -> "0"
},
firmwareVersion = card.firmwareVersion.stringValue,
cardBlockchain = walletData?.blockchain,
signedHashesList = card.wallets.map {

View file

@ -10,6 +10,7 @@ import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.pagination.*
import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
internal class DefaultMarketsTokenRepository(
@ -19,6 +20,7 @@ internal class DefaultMarketsTokenRepository(
) : MarketsTokenRepository {
private val tokenListConverter = TokenMarketListConverter()
private val tokenChartConverter = TokenChartConverter()
private fun createTokenMarketsFetcher(firstBatchSize: Int, nextBatchSize: Int) = LimitOffsetBatchFetcher(
prefetchDistance = firstBatchSize,
@ -41,9 +43,9 @@ internal class DefaultMarketsTokenRepository(
interval = request.params.priceChangeInterval.toRequestParam(),
order = request.params.order.toRequestParam(),
search = searchText,
generalCoins = request.params.showUnder100kMarketCapTokens.not(),
offset = request.offset,
limit = request.limit,
timestamp = if (isFirstBatchFetching) null else requestTimeStamp.get(),
).getOrThrow()
}
@ -57,7 +59,7 @@ internal class DefaultMarketsTokenRepository(
}
if (isFirstBatchFetching) {
requestTimeStamp.set(0) // TODO when backend is ready
requestTimeStamp.set(res.timestamp ?: 0)
}
val last = res.tokens.size < request.limit
@ -81,12 +83,28 @@ internal class DefaultMarketsTokenRepository(
marketsApi = marketsApi,
)
val atomicInteger = AtomicInteger(0)
return BatchListSource(
fetchDispatcher = dispatcherProvider.io,
context = batchingContext,
generateNewKey = { it.size },
generateNewKey = { atomicInteger.getAndIncrement() },
batchFetcher = createTokenMarketsFetcher(firstBatchSize = firstBatchSize, nextBatchSize = nextBatchSize),
updateFetcher = tokenMarketsUpdateFetcher,
).toBatchFlow()
}
override suspend fun getChart(
fiatCurrencyCode: String,
interval: PriceChangeInterval,
tokenId: String,
): TokenChart {
val response = marketsApi.getCoinChart(
currency = fiatCurrencyCode,
coinId = tokenId,
interval = interval.toRequestParam(),
)
return tokenChartConverter.convert(interval, response.getOrThrow())
}
}

View file

@ -1,6 +1,6 @@
package com.tangem.data.markets
import com.tangem.data.markets.converters.TokenListChartConverter
import com.tangem.data.markets.converters.TokenChartConverter
import com.tangem.data.markets.converters.TokenMarketChartsConverter
import com.tangem.data.markets.converters.TokenQuotesConverter
import com.tangem.data.markets.converters.toRequestParam
@ -21,7 +21,7 @@ internal class MarketsBatchUpdateFetcher(
private val tangemTechApi: TangemTechApi,
) : BatchUpdateFetcher<Int, List<TokenMarket>, TokenMarketUpdateRequest> {
private val tokenListChartsConverter = TokenMarketChartsConverter(TokenListChartConverter())
private val tokenListChartsConverter = TokenMarketChartsConverter(TokenChartConverter())
private val tokenQuotesConverter = TokenQuotesConverter()
override suspend fun BatchUpdateFetcher.UpdateContext<Int, List<TokenMarket>>.fetchUpdateAsync(

View file

@ -4,13 +4,13 @@ import com.tangem.datasource.api.markets.models.response.TokenMarketChartRespons
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenChart
class TokenListChartConverter {
class TokenChartConverter {
fun convert(interval: PriceChangeInterval, value: TokenMarketChartResponse): TokenChart {
return TokenChart(
interval = interval,
priceY = value.prices.values.toList(),
timeStamp = value.prices.keys.toList(),
timeStamps = value.prices.keys.toList(),
)
}
}

View file

@ -20,7 +20,7 @@ fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) {
fun PriceChangeInterval.toRequestParam(): String = when (this) {
PriceChangeInterval.H24 -> "24h"
PriceChangeInterval.WEEK -> "1w"
PriceChangeInterval.MONTH -> "30d"
PriceChangeInterval.MONTH -> "1m"
PriceChangeInterval.MONTH3 -> "3m"
PriceChangeInterval.MONTH6 -> "6m"
PriceChangeInterval.YEAR -> "1y"

View file

@ -3,31 +3,38 @@ 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
import com.tangem.domain.markets.TokenMarketListConfig
class TokenMarketChartsConverter(
private val tokenListChartConverter: TokenListChartConverter,
private val tokenChartConverter: TokenChartConverter,
) {
fun convert(
chartsToCopy: TokenMarket.Charts,
tokenId: String,
interval: PriceChangeInterval,
interval: TokenMarketListConfig.Interval,
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),
TokenMarketListConfig.Interval.H24 -> chartsToCopy.copy(
h24 = tokenChartConverter.convert(interval.toPriceChangeInterval(), prices),
)
PriceChangeInterval.WEEK -> chartsToCopy.copy(
week = tokenListChartConverter.convert(interval, prices),
TokenMarketListConfig.Interval.WEEK -> chartsToCopy.copy(
week = tokenChartConverter.convert(interval.toPriceChangeInterval(), prices),
)
PriceChangeInterval.MONTH -> chartsToCopy.copy(
month = tokenListChartConverter.convert(interval, prices),
TokenMarketListConfig.Interval.MONTH -> chartsToCopy.copy(
month = tokenChartConverter.convert(interval.toPriceChangeInterval(), prices),
)
else -> error("unsupported interval=$interval. This shouldn't have happened.")
}
}
private fun TokenMarketListConfig.Interval.toPriceChangeInterval(): PriceChangeInterval = when (this) {
TokenMarketListConfig.Interval.H24 -> PriceChangeInterval.H24
TokenMarketListConfig.Interval.WEEK -> PriceChangeInterval.WEEK
TokenMarketListConfig.Interval.MONTH -> PriceChangeInterval.MONTH
}
}

View file

@ -22,6 +22,7 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.staking)
implementation(projects.domain.wallets.models)
implementation(projects.domain.legacy)
/** Feature Api modules */
implementation(projects.features.staking.api)
@ -40,6 +41,8 @@ dependencies {
implementation(projects.libs.blockchainSdk)
implementation(deps.tangem.card.core)
implementation(deps.tangem.blockchain) {
exclude(module = "joda-time")
}

View file

@ -1,8 +1,10 @@
package com.tangem.data.staking
import android.util.Base64
import arrow.core.raise.catch
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.common.extensions.toCompressedPublicKey
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.staking.converters.*
import com.tangem.data.staking.converters.action.ActionStatusConverter
@ -23,7 +25,10 @@ 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.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata
import com.tangem.domain.staking.model.stakekit.NetworkType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.YieldBalanceList
@ -37,10 +42,11 @@ 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.walletmanager.WalletManagersFacade
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 com.tangem.utils.extensions.orZero
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -55,6 +61,7 @@ internal class DefaultStakingRepository(
private val cacheRegistry: CacheRegistry,
private val dispatchers: CoroutineDispatcherProvider,
private val stakingFeatureToggle: StakingFeatureToggles,
private val walletManagersFacade: WalletManagersFacade,
) : StakingRepository {
private val stakingNetworkTypeConverter = StakingNetworkTypeConverter()
@ -129,7 +136,7 @@ internal class DefaultStakingRepository(
val yield = getYield(cryptoCurrencyId, symbol)
StakingEntryInfo(
interestRate = yield.apy,
interestRate = requireNotNull(yield.validators.maxByOrNull { it.apr.orZero() }?.apr),
periodInDays = yield.metadata.cooldownPeriod.days,
tokenSymbol = yield.token.symbol,
)
@ -160,12 +167,30 @@ internal class DefaultStakingRepository(
}
}
override suspend fun createAction(params: ActionParams): StakingAction {
override suspend fun createAction(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): StakingAction {
return withContext(dispatchers.io) {
val response = when (params.actionCommonType) {
StakingActionCommonType.ENTER -> stakeKitApi.createEnterAction(createActionRequestBody(params))
StakingActionCommonType.EXIT -> stakeKitApi.createExitAction(createActionRequestBody(params))
StakingActionCommonType.PENDING -> stakeKitApi.createPendingAction(
StakingActionCommonType.ENTER -> stakeKitApi.createEnterAction(
createActionRequestBody(
userWalletId,
network,
params,
),
)
StakingActionCommonType.EXIT -> stakeKitApi.createExitAction(
createActionRequestBody(
userWalletId,
network,
params,
),
)
StakingActionCommonType.PENDING_OTHER,
StakingActionCommonType.PENDING_REWARDS,
-> stakeKitApi.createPendingAction(
createPendingActionRequestBody(params),
)
}
@ -174,12 +199,30 @@ internal class DefaultStakingRepository(
}
}
override suspend fun estimateGas(params: ActionParams): StakingGasEstimate {
override suspend fun estimateGas(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): StakingGasEstimate {
return withContext(dispatchers.io) {
val gasEstimateDTO = when (params.actionCommonType) {
StakingActionCommonType.ENTER -> stakeKitApi.estimateGasOnEnter(createActionRequestBody(params))
StakingActionCommonType.EXIT -> stakeKitApi.estimateGasOnExit(createActionRequestBody(params))
StakingActionCommonType.PENDING -> stakeKitApi.estimateGasOnPending(
StakingActionCommonType.ENTER -> stakeKitApi.estimateGasOnEnter(
createActionRequestBody(
userWalletId,
network,
params,
),
)
StakingActionCommonType.EXIT -> stakeKitApi.estimateGasOnExit(
createActionRequestBody(
userWalletId,
network,
params,
),
)
StakingActionCommonType.PENDING_REWARDS,
StakingActionCommonType.PENDING_OTHER,
-> stakeKitApi.estimateGasOnPending(
createPendingActionRequestBody(params),
)
}
@ -435,12 +478,19 @@ internal class DefaultStakingRepository(
}
}
private fun createActionRequestBody(params: ActionParams): ActionRequestBody {
private suspend fun createActionRequestBody(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): ActionRequestBody {
return ActionRequestBody(
integrationId = params.integrationId,
addresses = Address(params.address),
addresses = Address(
address = params.address,
additionalAddresses = createAdditionalAddresses(userWalletId, network, params),
),
args = ActionRequestBodyArgs(
amount = params.amount.toFormattedString(params.token.decimals),
amount = params.amount.toPlainString(),
inputToken = tokenConverter.convertBack(params.token),
validatorAddress = params.validatorAddress,
),
@ -453,12 +503,29 @@ internal class DefaultStakingRepository(
type = params.type ?: StakingActionType.UNKNOWN,
passthrough = params.passthrough.orEmpty(),
args = ActionRequestBodyArgs(
amount = params.amount.toFormattedString(params.token.decimals),
amount = params.amount.toPlainString(),
validatorAddress = params.validatorAddress,
),
)
}
private suspend fun createAdditionalAddresses(
userWalletId: UserWalletId,
network: Network,
params: ActionParams,
): Address.AdditionalAddresses? {
val selectedWallet = walletManagersFacade.getOrCreateWalletManager(userWalletId, network)
return when (params.token.network) {
NetworkType.COSMOS -> Address.AdditionalAddresses(
cosmosPubKey = Base64.encodeToString(
/* input = */ selectedWallet?.wallet?.publicKey?.blockchainKey?.toCompressedPublicKey(),
/* flags = */ Base64.NO_WRAP,
),
)
else -> null
}
}
override fun isStakeMoreAvailable(networkId: Network.ID): Boolean {
val blockchain = Blockchain.fromId(networkId.value)
return when (blockchain) {
@ -502,23 +569,24 @@ internal class DefaultStakingRepository(
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 BINANCE_INTEGRATION_ID = "bsc-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"
// uncomment items as implementation is ready
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,
// 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.BSC.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,
)
}
}

View file

@ -39,14 +39,18 @@ class YieldConverter(
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) },
args = enterDTO.args
.mapKeys { convertArgType(it.key) }
.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) },
additionalAddresses = addressesDTO.additionalAddresses
?.mapKeys { convertArgType(it.key) }
?.mapValues { convertAddressArgument(it.value) },
)
}
@ -122,4 +126,12 @@ class YieldConverter(
else -> Yield.RewardType.UNKNOWN
}
}
private fun convertArgType(value: String): Yield.Args.ArgType {
return when (value) {
"address" -> Yield.Args.ArgType.ADDRESS
"amount" -> Yield.Args.ArgType.AMOUNT
else -> Yield.Args.ArgType.UNKNOWN
}
}
}

View file

@ -13,6 +13,7 @@ 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.domain.walletmanager.WalletManagersFacade
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -35,6 +36,7 @@ internal object StakingDataModule {
dispatchers: CoroutineDispatcherProvider,
stakingFeatureToggle: StakingFeatureToggles,
cacheRegistry: CacheRegistry,
walletManagersFacade: WalletManagersFacade,
): StakingRepository {
return DefaultStakingRepository(
stakeKitApi = stakeKitApi,
@ -44,6 +46,7 @@ internal object StakingDataModule {
dispatchers = dispatchers,
cacheRegistry = cacheRegistry,
stakingFeatureToggle = stakingFeatureToggle,
walletManagersFacade = walletManagersFacade,
)
}

View file

@ -100,7 +100,7 @@ internal class DefaultNetworksRepository(
override fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean {
val blockchain = Blockchain.fromNetworkId(network.id.value)
return blockchain == Blockchain.Aptos
return REQUIRED_ACCOUNT_WITHOUT_RESERVE_BLOCKCHAINS.contains(blockchain)
}
override suspend fun getNetworkAddresses(
@ -345,4 +345,9 @@ internal class DefaultNetworksRepository(
private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId, network: Network): String {
return "network_status_${userWalletId}_${network.id.value}_${network.derivationPath.value}"
}
private companion object {
val REQUIRED_ACCOUNT_WITHOUT_RESERVE_BLOCKCHAINS = listOf(Blockchain.Aptos, Blockchain.Filecoin)
}
}