Updated on 2026-08-14

This commit is contained in:
Tangem 2024-10-29 15:22:03 +03:00
commit 6d3b416f0c
26 changed files with 231 additions and 517 deletions

View file

@ -357,7 +357,10 @@ internal object TokensDomainModule {
@Provides
@Singleton
fun provideGetCurrencyCheckUseCase(currencyChecksRepository: CurrencyChecksRepository): GetCurrencyCheckUseCase {
return GetCurrencyCheckUseCase(currencyChecksRepository)
fun provideGetCurrencyCheckUseCase(
currencyChecksRepository: CurrencyChecksRepository,
dispatchers: CoroutineDispatcherProvider,
): GetCurrencyCheckUseCase {
return GetCurrencyCheckUseCase(currencyChecksRepository, dispatchers)
}
}

View file

@ -67,7 +67,7 @@ object NotificationsFactory {
if (!isAccountFunded && reserveAmount != null && reserveAmount > sendingAmount) {
add(
NotificationUM.Error.ReserveAmount(
sendingAmount.format {
reserveAmount.format {
crypto(cryptoCurrency)
},
),

View file

@ -179,6 +179,7 @@ sealed class AnalyticsParam {
const val ERROR_CODE = "Error Code"
const val ERROR_KEY = "Error Key"
const val ERROR_TYPE = "Error Type"
const val ERROR_MESSAGE = "Error Message"
const val CREATION_TYPE = "Creation type"
const val DAPP_NAME = "DApp Name"
const val DAPP_URL = "DApp Url"

View file

@ -2,6 +2,7 @@ package com.tangem.core.analytics.di
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.api.ParamsInterceptorHolder
import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.domain.analytics.repository.AnalyticsRepository
import dagger.Module
@ -20,6 +21,12 @@ internal object AnalyticsModule {
return Analytics // todo replace after refactoring calling Analytics in whole project
}
@Singleton
@Provides
fun provideParamsInterceptorHolder(): ParamsInterceptorHolder {
return Analytics // todo replace after refactoring calling Analytics in whole project
}
@Provides
fun provideOneTimeEventFilter(analyticsRepository: AnalyticsRepository): OneTimeEventFilter {
return OneTimeEventFilter(analyticsRepository)

View file

@ -11,8 +11,6 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.RewardT
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.ValidatorDTO.ValidatorStatusDTO
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO
import com.tangem.datasource.api.stakekit.models.response.model.error.AccessDeniedErrorTypeDTO
import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorMessageDTO
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionStatusDTO
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionTypeDTO
@ -40,9 +38,6 @@ fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder {
StakingTransactionStatusDTO::class.java to StakingTransactionStatusDTO.UNKNOWN,
StakingTransactionTypeDTO::class.java to StakingTransactionTypeDTO.UNKNOWN,
ValidatorStatusDTO::class.java to ValidatorStatusDTO.UNKNOWN,
// error enums
AccessDeniedErrorTypeDTO::class.java to AccessDeniedErrorTypeDTO.UNKNOWN,
StakeKitErrorMessageDTO::class.java to StakeKitErrorMessageDTO.UNKNOWN,
)
return apply {

View file

@ -3,18 +3,21 @@ package com.tangem.datasource.api.stakekit.models.response.model.error
import com.squareup.moshi.Json
class StakeKitErrorResponse(
@Json(name = "message")
val message: String? = null,
@Json(name = "code")
val code: Int? = null,
@Json(name = "path")
val path: String? = null,
@Json(name = "details")
val details: StakeKitErrorDetailsDTO? = null,
@Json(name = "message")
val message: StakeKitErrorMessageDTO? = null,
@Json(name = "level")
val level: String? = null, // unused
// 403
@Json(name = "type")
val type: AccessDeniedErrorTypeDTO? = null,
@Json(name = "code")
val code: String? = null,
val type: String? = null,
@Json(name = "countryCode")
val countryCode: String?,
@Json(name = "regionCode")
@ -30,135 +33,4 @@ data class StakeKitErrorDetailsDTO(
val amount: String? = null,
@Json(name = "yieldId")
val yieldId: String? = null,
)
enum class AccessDeniedErrorTypeDTO {
@Json(name = "GEO_LOCATION")
GEO_LOCATION,
UNKNOWN,
}
enum class StakeKitErrorMessageDTO {
@Json(name = "MissingArgumentsError")
MISSING_ARGUMENTS_ERROR,
@Json(name = "MinimumAmountNotReached")
MINIMUM_AMOUNT_NOT_REACHED,
@Json(name = "YieldUnderMaintenanceError")
YIELD_UNDER_MAINTENANCE_ERROR,
@Json(name = "InsufficientFundsError")
INSUFFICIENT_FUNDS_ERROR,
@Json(name = "StakedPositionNotFoundError")
STAKED_POSITION_NOT_FOUND_ERROR,
@Json(name = "InvalidAmountSubmittedError")
INVALID_AMOUNT_SUBMITTED_ERROR,
@Json(name = "BalanceUnavailableError")
BALANCE_UNAVAILABLE_ERROR,
@Json(name = "GasPriceUnavailableError")
GAS_PRICE_UNAVAILABLE_ERROR,
@Json(name = "NotImplementedError")
NOT_IMPLEMENTED_ERROR,
@Json(name = "TokenNotFoundError")
TOKEN_NOT_FOUND_ERROR,
@Json(name = "BroadcastTransactionError")
BROADCAST_TRANSACTION_ERROR,
@Json(name = "MissingGasPriceStrategyError")
MISSING_GAS_PRICE_STRATEGY_ERROR,
@Json(name = "SubstrateMalformedTransactionHashError")
SUBSTRATE_MALFORMED_TRANSACTION_HASH_ERROR,
@Json(name = "TronMaximumAmountOfValidatorsExceededError")
TRON_MAXIMUM_AMOUNT_OF_VALIDATORS_EXCEEDED_ERROR,
@Json(name = "SubstratePoolNotFoundError")
SUBSTRATE_POOL_NOT_FOUND_ERROR,
@Json(name = "SubstrateBondedAmountTooLowError")
SUBSTRATE_BONDED_AMOUNT_TOO_LOW_ERROR,
@Json(name = "TronMissingResourceTypeArgumentError")
TRON_MISSING_RESOURCE_TYPE_ARGUMENT_ERROR,
@Json(name = "AaveV3PoolFrozenError")
AAVE_V3_POOL_FROZEN_ERROR,
@Json(name = "AaveV3TokenPairNotFoundError")
AAVE_V3_TOKEN_PAIR_NOT_FOUND_ERROR,
@Json(name = "YearnVaultAtMaxCapacityError")
YEARN_VAULT_AT_MAX_CAPACITY_ERROR,
@Json(name = "StETHNoWithdrawalRequestsFoundError")
STETH_NO_WITHDRAWAL_REQUESTS_FOUND_ERROR,
@Json(name = "MorphoLendingPoolPausedError")
MORPHO_LENDING_POOL_PAUSED_ERROR,
@Json(name = "NonceUnavailableError")
NONCE_UNAVAILABLE_ERROR,
@Json(name = "CosmosAcccountNotFoundError")
COSMOS_ACCOUNT_NOT_FOUND_ERROR,
@Json(name = "AvalancheMissingAdditionalAddressesArgumentError")
AVALANCHE_MISSING_ADDITIONAL_ADDRESSES_ARGUMENT_ERROR,
@Json(name = "AvalancheValidatorInfoNotFoundError")
AVALANCHE_VALIDATOR_INFO_NOT_FOUND_ERROR,
@Json(name = "SolanaTransactionSignatureVerificationFailureError")
SOLANA_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE_ERROR,
@Json(name = "SolanaUnableTocreateStakeAccountError")
SOLANA_UNABLE_TO_CREATE_STAKE_ACCOUNT_ERROR,
@Json(name = "SolanaStakeAmountTooLowError")
SOLANA_STAKE_AMOUNT_TOO_LOW_ERROR,
@Json(name = "SolanaUnstakeAmountTooLowError")
SOLANA_UNSTAKE_AMOUNT_TOO_LOW_ERROR,
@Json(name = "SolanaStakeAccountsNotFoundError")
SOLANA_STAKE_ACCOUNTS_NOT_FOUND_ERROR,
@Json(name = "SolanaEligibleStakeAccountsNotFoundError")
SOLANA_ELIGIBLE_STAKE_ACCOUNTS_NOT_FOUND_ERROR,
@Json(name = "TezosNoBalanceDelegatedError")
TEZOS_NO_BALANCE_DELEGATED_ERROR,
@Json(name = "TezosMissingPubkeyArgumentError")
TEZOS_MISSING_PUBKEY_ARGUMENT_ERROR,
@Json(name = "TezosEstimateRevealGasLimitError")
TEZOS_ESTIMATE_REVEAL_GAS_LIMIT_ERROR,
@Json(name = "TezosBalanceAlreadyDelegatedError")
TEZOS_BALANCE_ALREADY_DELEGATED_ERROR,
@Json(name = "BinanceAccountNotFoundError")
BINANCE_ACCOUNT_NOT_FOUND_ERROR,
@Json(name = "BinanceMissingAccountNumberOrSequenceError")
BINANCE_MISSING_ACCOUNT_NUMBER_OR_SEQUENCE_ERROR,
@Json(name = "GRTStakingDisabledError")
GRT_STAKING_DISABLED_ERROR,
@Json(name = "GRTStakingDisabledLedgerLiveError")
GRT_STAKING_DISABLED_LEDGER_LIVE_ERROR,
UNKNOWN,
}
)

View file

@ -339,7 +339,7 @@
<string name="manage_tokens_list_header_title">\nCoinMarketCap</string>
<string name="manage_tokens_network_selector_native_subtitle">Blockchain, auf der die Kryptowährung ursprünglich erstellt wurde</string>
<string name="manage_tokens_network_selector_native_title">Natives Netzwerk</string>
<string name="manage_tokens_network_selector_non_native_info">Die Verwendung von nicht nativen Netzwerken für Token ermöglicht eine blockchainübergreifende Interoperabilität, sodass Vermögenswerte in verschiedenen dezentralen Anwendungen und Smart Contracts plattformübergreifend genutzt werden können. Dies erfordert jedoch häufig eine Verwahrstelle oder einen intelligenten Vertrag, um den ursprünglichen Vermögenswert sicher zu verwahren, was zu einer Zentralisierung und einem Kontrahentenrisiko führt.</string>
<string name="manage_tokens_network_selector_non_native_info">Die Verwendung nicht-nativer Netzwerke für Token ermöglicht Interoperabilität zwischen Blockchains, sodass Vermögenswerte plattformübergreifend in verschiedenen dezentralen Anwendungen und Smart Contracts genutzt werden können. Dies erfordert jedoch häufig einen Verwahrer oder Smart Contract, um den ursprünglichen Vermögenswert sicher aufzubewahren, was zu Zentralisierung und Kontrahentenrisiken führt.</string>
<string name="manage_tokens_network_selector_non_native_subtitle">Nicht die ursprüngliche oder primäre Blockchain, in der der Token gehostet wird</string>
<string name="manage_tokens_network_selector_non_native_title">Nicht-native Netzwerke</string>
<string name="manage_tokens_network_selector_title">Netzwerke auswählen</string>
@ -710,7 +710,7 @@
<string name="staking_details_market_rating">Marktbewertung</string>
<string name="staking_details_metrics_block_header">Metriken</string>
<string name="staking_details_minimum_requirement">Mindestanforderungen</string>
<string name="staking_details_no_rewards_to_claim">Keine Belohnungen</string>
<string name="staking_details_no_rewards_to_claim">Keine Belohnungen verfügbar</string>
<string name="staking_details_reward_claiming">Belohnungen beanspruchen</string>
<string name="staking_details_reward_claiming_info">Eine Möglichkeit, Staking-Belohnungen zu erhalten. Es kann automatisch oder manuell beansprucht werden.</string>
<string name="staking_details_reward_schedule">Belohnungszeitplan</string>
@ -760,7 +760,7 @@
<string name="staking_reward_schedule_day">Täglich</string>
<string name="staking_reward_schedule_each">Alle %s</string>
<string name="staking_reward_schedule_each_day">Täglich</string>
<string name="staking_reward_schedule_each_days">Alle %s</string>
<string name="staking_reward_schedule_each_days">Alle %s Tage</string>
<string name="staking_reward_schedule_each_plural">Jeder</string>
<string name="staking_reward_schedule_epoch">Pro Epoche</string>
<string name="staking_reward_schedule_era">Pro Ära</string>

View file

@ -520,7 +520,7 @@
</plurals>
<string name="onboarding_seed_generate_title">Ваша seed-фраза</string>
<plurals name="onboarding_seed_generate_words_count">
<item quantity="one"></item>
<item quantity="one">слово</item>
<item quantity="few">%d слова</item>
<item quantity="many">%d слів</item>
<item quantity="other">%d слів</item>
@ -741,7 +741,7 @@
<string name="staking_details_unbonding_period_info">Період, який ви повинні чекати після запиту на виведення коштів зі стейкінгу, перш ніж токени стануть доступними.</string>
<string name="staking_details_warmup_period">Період блокування</string>
<string name="staking_details_warmup_period_info">Відведений час для активації участі в стейкінгу.</string>
<string name="staking_give_permission_fee_footer">Комісія мережі за схвалення токену, щоб верифікувати, що саме ви даєте дозвіл на використання вашого токена для стейкінгу.</string>
<string name="staking_give_permission_fee_footer">Комісія за схвалення токену, щоб верифікувати, що саме ви дозволяєте використання токена для стейкінгу.</string>
<string name="staking_legal">Використовуючи функцію стейкінгу, ви погоджуєтесь з %1$s та %2$s</string>
<string name="staking_locked">Заблоковано</string>
<string name="staking_migrate">Перемістити</string>

View file

@ -1,19 +1,36 @@
package com.tangem.data.staking
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.staking.converters.error.StakeKitErrorConverter
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.repositories.StakingErrorResolver
internal class DefaultStakingErrorResolver(
private val analyticsEventHandler: AnalyticsEventHandler,
private val stakeKitErrorConverter: StakeKitErrorConverter,
) : StakingErrorResolver {
override fun resolve(throwable: Throwable): StakingError {
return if (throwable is ApiResponseError.HttpException) {
val error = if (throwable is ApiResponseError.HttpException) {
stakeKitErrorConverter.convert(throwable.errorBody.orEmpty())
} else {
StakingError.UnknownError()
StakingError.DomainError(throwable.message)
}
when (error) {
is StakingError.StakeKitApiError -> {
analyticsEventHandler.send(StakingAnalyticsEvent.StakeKitApiError(error))
}
is StakingError.StakeKitUnknownError -> {
analyticsEventHandler.send(StakingAnalyticsEvent.StakeKitApiUnknownError(error))
}
else -> {
// intentionally do nothing
}
}
return error
}
}

View file

@ -127,7 +127,7 @@ class YieldConverter(
stakedBalance = validatorDTO.stakedBalance,
votingPower = validatorDTO.votingPower,
preferred = validatorDTO.preferred,
isStrategicPartner = isStrategicPartner(address),
isStrategicPartner = isStrategicPartner(validatorDTO.address, validatorDTO.name),
)
}
@ -180,11 +180,17 @@ class YieldConverter(
}
}
private fun isStrategicPartner(validatorAddress: String): Boolean = PARTNERS.any { it == validatorAddress }
private fun isStrategicPartner(validatorAddress: String?, validatorName: String): Boolean {
return PARTNERS.any { it == validatorAddress } || PARTNERS_NAMES.any { it.equals(validatorName, true) }
}
private companion object {
val PARTNERS = listOf(
"cosmosvaloper1wrx0x9m9ykdhw9sg04v7uljme53wuj03aa5d4f",
"H2tJNyMHnRF6ahCQLQ1sSycM4FGchymuzyYzUqKEuydk",
)
val PARTNERS_NAMES = listOf(
"Meria",
)
}
}

View file

@ -1,8 +1,6 @@
package com.tangem.data.staking.converters.error
import com.squareup.moshi.JsonAdapter
import com.tangem.datasource.api.stakekit.models.response.model.error.AccessDeniedErrorTypeDTO
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.stakekit.StakingError
import com.tangem.utils.converter.Converter
@ -11,105 +9,18 @@ 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(value)
val stakeKitErrorResponse = jsonAdapter.fromJson(value)
?: return StakingError.StakeKitUnknownError(value)
if (stakeKitErrorResponse.type == AccessDeniedErrorTypeDTO.GEO_LOCATION) {
StakingError.UnavailableDueToGeolocationError(
tags = stakeKitErrorResponse.tags ?: emptyList(),
)
}
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(value)
}
return StakingError.StakeKitApiError(
message = stakeKitErrorResponse.message,
code = stakeKitErrorResponse.code,
methodName = stakeKitErrorResponse.path,
)
} catch (e: Exception) {
StakingError.UnknownError(value)
StakingError.StakeKitUnknownError(value)
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.data.staking.di
import com.squareup.moshi.Moshi
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.staking.*
import com.tangem.data.staking.DefaultStakingErrorResolver
@ -83,10 +84,14 @@ internal object StakingDataModule {
@Provides
@Singleton
internal fun provideStakingErrorResolver(@NetworkMoshi moshi: Moshi): StakingErrorResolver {
internal fun provideStakingErrorResolver(
@NetworkMoshi moshi: Moshi,
analyticsEventHandler: AnalyticsEventHandler,
): StakingErrorResolver {
val jsonAdapter = moshi.adapter(StakeKitErrorResponse::class.java)
return DefaultStakingErrorResolver(
stakeKitErrorConverter = StakeKitErrorConverter(jsonAdapter),
analyticsEventHandler = analyticsEventHandler,
)
}
}

View file

@ -12,6 +12,7 @@ android {
dependencies {
api(projects.domain.staking.models)
api(projects.domain.core)
api(projects.core.analytics)
implementation(deps.kotlin.serialization)
implementation(deps.jodatime)

View file

@ -4,93 +4,17 @@ sealed class StakingError {
// region stakekit errors
data class MinimumAmountNotReachedError(val amount: String) : StakingError()
data class StakeKitApiError(
val message: String?,
val code: Int?,
val methodName: String?,
) : StakingError()
data class MissingArgumentsError(val arguments: String) : StakingError()
data class YieldUnderMaintenanceError(val yieldId: String) : StakingError()
data object InsufficientFundsError : StakingError()
data object StakedPositionNotFoundError : StakingError()
data object InvalidAmountSubmittedError : StakingError()
data object BalanceUnavailableError : StakingError()
data object GasPriceUnavailableError : StakingError()
data object NotImplementedError : StakingError()
data object TokenNotFoundError : StakingError()
data object BroadcastTransactionError : StakingError()
data object MissingGasPriceStrategyError : StakingError()
data object SubstrateMalformedTransactionHashError : StakingError()
data object TronMaximumAmountOfValidatorsExceededError : StakingError()
data object SubstratePoolNotFoundError : StakingError()
data object SubstrateBondedAmountTooLowError : StakingError()
data object TronMissingResourceTypeArgumentError : StakingError()
data object AaveV3PoolFrozenError : StakingError()
data object AaveV3TokenPairNotFoundError : StakingError()
data object YearnVaultAtMaxCapacityError : StakingError()
data object StETHNoWithdrawalRequestsFoundError : StakingError()
data object MorphoLendingPoolPausedError : StakingError()
data object NonceUnavailableError : StakingError()
data object CosmosAcccountNotFoundError : StakingError()
data object AvalancheMissingAdditionalAddressesArgumentError : StakingError()
data object AvalancheValidatorInfoNotFoundError : StakingError()
data object SolanaTransactionSignatureVerificationFailureError : StakingError()
data object SolanaUnableTocreateStakeAccountError : StakingError()
data object SolanaStakeAmountTooLowError : StakingError()
data object SolanaUnstakeAmountTooLowError : StakingError()
data object SolanaStakeAccountsNotFoundError : StakingError()
data object SolanaEligibleStakeAccountsNotFoundError : StakingError()
data object TezosNoBalanceDelegatedError : StakingError()
data object TezosMissingPubkeyArgumentError : StakingError()
data object TezosEstimateRevealGasLimitError : StakingError()
data object TezosBalanceAlreadyDelegatedError : StakingError()
data object BinanceAccountNotFoundError : StakingError()
data object BinanceMissingAccountNumberOrSequenceError : StakingError()
data object GRTStakingDisabledError : StakingError()
data object GRTStakingDisabledLedgerLiveError : StakingError()
data class UnavailableDueToGeolocationError(
val tags: List<String>,
data class StakeKitUnknownError(
val jsonString: String? = null,
) : StakingError()
// endregion
data class DataError(val cause: Throwable) : StakingError()
data class UnknownError(val message: String? = null) : StakingError()
data class DomainError(val message: String?) : StakingError()
}

View file

@ -1,11 +1,12 @@
package com.tangem.features.staking.impl.analytics
package com.tangem.domain.staking.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent.ButtonRewards.addIfValueIsNotNull
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
internal sealed class StakingAnalyticsEvents(
sealed class StakingAnalyticsEvent(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(
@ -16,37 +17,27 @@ internal sealed class StakingAnalyticsEvents(
data class StakingInfoScreenOpened(
val validatorsCount: Int,
val token: String,
) : StakingAnalyticsEvents(
) : StakingAnalyticsEvent(
event = "Staking Info Screen Opened",
params = mapOf(
"Validators Count" to validatorsCount.toString(),
AnalyticsParam.TOKEN_PARAM to token,
),
)
data class WhatIsStaking(val token: String) : StakingAnalyticsEvents(
data object WhatIsStaking : StakingAnalyticsEvent(
event = "Link - What Is Staking",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
),
)
data class AmountScreenOpened(val token: String) : StakingAnalyticsEvents(
data object AmountScreenOpened : StakingAnalyticsEvent(
event = "Amount Screen Opened",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
),
)
data class ConfirmationScreenOpened(
val token: String,
val validator: String,
val action: StakingActionType,
) : StakingAnalyticsEvents(
) : StakingAnalyticsEvent(
event = "Confirmation Screen Opened",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
"Validator" to validator,
"Action" to action.asAnalyticName,
),
@ -54,126 +45,108 @@ internal sealed class StakingAnalyticsEvents(
data class StakeInProgressScreenOpened(
val validator: String,
val token: String,
val action: StakingActionType,
) : StakingAnalyticsEvents(
) : StakingAnalyticsEvent(
event = "Stake In Progress Screen Opened",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
"Validator" to validator,
"Action" to action.asAnalyticName,
),
)
data class RewardScreenOpened(val token: String) : StakingAnalyticsEvents(
data object RewardScreenOpened : StakingAnalyticsEvent(
event = "Reward Screen Opened",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
),
)
data class AmountSelectCurrency(
val token: String,
val isAppCurrency: Boolean,
) : StakingAnalyticsEvents(
) : StakingAnalyticsEvent(
event = "Selected Currency",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
AnalyticsParam.TYPE to if (isAppCurrency) "App Currency" else "Token",
),
)
data class ButtonMax(val token: String) : StakingAnalyticsEvents(
data object ButtonMax : StakingAnalyticsEvent(
event = "Button - Max",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
),
)
data class ButtonCancel(
val source: StakeScreenSource,
val token: String,
) : StakingAnalyticsEvents(
) : StakingAnalyticsEvent(
event = "Button - Cancel",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
AnalyticsParam.SOURCE to source.name,
),
)
data class ValidatorChosen(
val token: String,
val validator: String,
) : StakingAnalyticsEvents(
) : StakingAnalyticsEvent(
event = "Validator Chosen",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
"Validator" to validator,
),
)
data class ButtonValidator(
val source: StakeScreenSource,
val token: String,
) : StakingAnalyticsEvents(
) : StakingAnalyticsEvent(
event = "Button - Validator",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
AnalyticsParam.SOURCE to source.name,
),
)
data class ButtonRewards(
val token: String,
) : StakingAnalyticsEvents(
data object ButtonRewards : StakingAnalyticsEvent(
event = "Button - Rewards",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
),
)
data class ButtonAction(
val action: StakingActionType,
val token: String,
val validator: String,
) : StakingAnalyticsEvents(
) : StakingAnalyticsEvent(
event = "Button - ${action.asAnalyticName}",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
"Validator" to validator,
),
)
data object ButtonShare : StakingAnalyticsEvents(event = "Button - Share")
data object ButtonShare : StakingAnalyticsEvent(event = "Button - Share")
data object ButtonExplore : StakingAnalyticsEvents(event = "Button - Explore")
data object ButtonExplore : StakingAnalyticsEvent(event = "Button - Explore")
data class StakekitError(
val token: String,
val stakeKitError: StakingError,
) : StakingAnalyticsEvents(
data class StakeKitApiError(
val stakingError: StakingError.StakeKitApiError,
) : StakingAnalyticsEvent(
event = "Errors",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
AnalyticsParam.ERROR_TYPE to if (stakeKitError is StakingError.UnknownError) {
stakeKitError.message ?: "Unknown"
} else {
stakeKitError.javaClass.simpleName
},
),
params = buildMap {
addIfValueIsNotNull(AnalyticsParam.ERROR_MESSAGE, stakingError.message)
addIfValueIsNotNull(AnalyticsParam.ERROR_CODE, stakingError.code)
addIfValueIsNotNull(AnalyticsParam.METHOD_NAME, stakingError.methodName)
},
)
data class TransactionError(
val token: String,
) : StakingAnalyticsEvents(
data class StakeKitApiUnknownError(
val stakeKitUnknownError: StakingError.StakeKitUnknownError,
) : StakingAnalyticsEvent(
event = "Errors",
params = buildMap {
addIfValueIsNotNull(AnalyticsParam.ERROR_DESCRIPTION, stakeKitUnknownError.jsonString)
},
)
fun MutableMap<String, String>.addIfValueIsNotNull(key: String, value: Any?) {
if (value != null) {
put(key, value.toString())
}
}
data object TransactionError : StakingAnalyticsEvent(
event = "Error - Transaction Rejected",
params = mapOf(AnalyticsParam.TOKEN_PARAM to token),
)
}
enum class StakeScreenSource {
Info,
Amount,
Confirmation,
Validators,
Info, Amount, Confirmation, Validators,
}

View file

@ -8,4 +8,5 @@ data class CryptoCurrencyCheck(
val reserveAmount: BigDecimal?,
val existentialDeposit: BigDecimal?,
val utxoAmountLimit: UtxoAmountLimit?,
val isAccountFunded: Boolean,
)

View file

@ -4,10 +4,13 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import java.math.BigDecimal
class GetCurrencyCheckUseCase(
private val currencyChecksRepository: CurrencyChecksRepository,
private val dispatchers: CoroutineDispatcherProvider,
) {
suspend operator fun invoke(
@ -15,27 +18,38 @@ class GetCurrencyCheckUseCase(
currencyStatus: CryptoCurrencyStatus,
amount: BigDecimal?,
fee: BigDecimal?,
recipientAddress: String? = null,
): CryptoCurrencyCheck {
val network = currencyStatus.currency.network
val dustValue = currencyChecksRepository.getDustValue(userWalletId, network)
val reserveAmount = currencyChecksRepository.getReserveAmount(userWalletId, network)
val existentialDeposit = currencyChecksRepository.getExistentialDeposit(userWalletId, network)
val utxoAmountLimit = if (amount != null && fee != null) {
currencyChecksRepository.checkUtxoAmountLimit(
userWalletId = userWalletId,
network = network,
amount = amount,
fee = fee,
)
} else {
null
}
return withContext(dispatchers.io) {
val network = currencyStatus.currency.network
val dustValue = currencyChecksRepository.getDustValue(userWalletId, network)
val reserveAmount = currencyChecksRepository.getReserveAmount(userWalletId, network)
val existentialDeposit = currencyChecksRepository.getExistentialDeposit(userWalletId, network)
val isAccountFunded = recipientAddress?.let {
currencyChecksRepository.checkIfAccountFunded(
userWalletId,
network,
recipientAddress,
)
} ?: false
val utxoAmountLimit = if (amount != null && fee != null) {
currencyChecksRepository.checkUtxoAmountLimit(
userWalletId = userWalletId,
network = network,
amount = amount,
fee = fee,
)
} else {
null
}
return CryptoCurrencyCheck(
dustValue = dustValue,
reserveAmount = reserveAmount,
existentialDeposit = existentialDeposit,
utxoAmountLimit = utxoAmountLimit,
)
CryptoCurrencyCheck(
dustValue = dustValue,
reserveAmount = reserveAmount,
existentialDeposit = existentialDeposit,
utxoAmountLimit = utxoAmountLimit,
isAccountFunded = isAccountFunded,
)
}
}
}

View file

@ -45,7 +45,7 @@ import kotlinx.coroutines.flow.map
import java.math.BigDecimal
@Suppress("LongParameterList", "LargeClass")
internal class SendNotificationFactory constructor(
internal class SendNotificationFactory(
private val analyticsEventHandler: AnalyticsEventHandler,
private val validateTransactionUseCase: ValidateTransactionUseCase,
private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase,
@ -90,11 +90,14 @@ internal class SendNotificationFactory constructor(
reduceAmountBy = reduceAmountBy,
)
val feeError = (feeState.feeSelectorState as? FeeSelectorState.Error)?.error
val recipientAddress = state.recipientState?.addressTextField?.value
val currencyCheck = getCurrencyCheckUseCase(
userWalletId = userWalletId,
currencyStatus = cryptoCurrencyStatus,
amount = sendingAmount,
fee = feeValue,
recipientAddress = recipientAddress,
)
buildList {
addErrorNotifications(
@ -187,7 +190,7 @@ internal class SendNotificationFactory constructor(
reserveAmount = currencyCheck.reserveAmount,
sendingAmount = sendingAmount,
cryptoCurrency = currency,
isAccountFunded = false,
isAccountFunded = currencyCheck.isAccountFunded,
)
}

View file

@ -0,0 +1,23 @@
package com.tangem.features.staking.impl.analytics
import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
class StakingParamsInterceptor(private val tokenSymbol: String) : ParamsInterceptor {
override fun id() = ID
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean {
return event is StakingAnalyticsEvent
}
override fun intercept(params: MutableMap<String, String>) {
params[AnalyticsParam.TOKEN_PARAM] = tokenSymbol
}
companion object {
const val ID = "StakingParamsInterceptorId"
}
}

View file

@ -7,8 +7,8 @@ import com.tangem.core.analytics.models.Basic
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.staking.impl.analytics.StakeScreenSource
import com.tangem.features.staking.impl.analytics.StakingAnalyticsEvents
import com.tangem.domain.staking.analytics.StakeScreenSource
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
import com.tangem.features.staking.impl.presentation.state.*
internal class StakingAnalyticSender(
@ -24,9 +24,8 @@ internal class StakingAnalyticSender(
?.size ?: 0
analyticsEventHandler.send(
StakingAnalyticsEvents.StakingInfoScreenOpened(
StakingAnalyticsEvent.StakingInfoScreenOpened(
validatorsCount = validatorCount,
token = value.cryptoCurrencySymbol,
),
)
}
@ -39,8 +38,7 @@ internal class StakingAnalyticSender(
if (confirmationState?.innerState == InnerConfirmationStakingState.COMPLETED) return
analyticsEventHandler.send(
StakingAnalyticsEvents.ConfirmationScreenOpened(
token = value.cryptoCurrencySymbol,
StakingAnalyticsEvent.ConfirmationScreenOpened(
validator = validatorName,
action = getStakingActionType(value),
),
@ -49,7 +47,7 @@ internal class StakingAnalyticSender(
fun screenCancel(value: StakingUiState) {
analyticsEventHandler.send(
StakingAnalyticsEvents.ButtonCancel(
StakingAnalyticsEvent.ButtonCancel(
source = when (value.currentStep) {
StakingStep.InitialInfo -> StakeScreenSource.Info
StakingStep.Amount -> StakeScreenSource.Amount
@ -59,7 +57,6 @@ internal class StakingAnalyticSender(
StakingStep.RewardsValidators,
-> StakeScreenSource.Validators
},
token = value.cryptoCurrencyName,
),
)
}
@ -93,9 +90,8 @@ internal class StakingAnalyticSender(
),
)
analyticsEventHandler.send(
StakingAnalyticsEvents.StakeInProgressScreenOpened(
StakingAnalyticsEvent.StakeInProgressScreenOpened(
validator = validatorName,
token = value.cryptoCurrencySymbol,
action = getStakingActionType(value),
),
)
@ -106,9 +102,8 @@ internal class StakingAnalyticSender(
val validatorName = validatorState?.chosenValidator?.name ?: return
analyticsEventHandler.send(
StakingAnalyticsEvents.ButtonAction(
StakingAnalyticsEvent.ButtonAction(
action = getStakingActionType(value),
token = value.cryptoCurrencySymbol,
validator = validatorName,
),
)

View file

@ -3,7 +3,7 @@ package com.tangem.features.staking.impl.presentation.state
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.analytics.StakingAnalyticsEvents
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
import com.tangem.features.staking.impl.analytics.utils.StakingAnalyticSender
internal class StakingStateRouter(
@ -68,16 +68,12 @@ internal class StakingStateRouter(
}
fun showRewardsValidators() {
analyticsEventsHandler.send(
StakingAnalyticsEvents.RewardScreenOpened(stateController.value.cryptoCurrencySymbol),
)
analyticsEventsHandler.send(StakingAnalyticsEvent.RewardScreenOpened)
stateController.update { it.copy(currentStep = StakingStep.RewardsValidators) }
}
private fun showAmount() {
analyticsEventsHandler.send(
StakingAnalyticsEvents.AmountScreenOpened(stateController.value.cryptoCurrencySymbol),
)
analyticsEventsHandler.send(StakingAnalyticsEvent.AmountScreenOpened)
stateController.update { it.copy(currentStep = StakingStep.Amount) }
}

View file

@ -135,7 +135,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor(
}
if (result.isNullOrEmpty()) {
onStakingFeeError(StakingError.UnknownError())
onStakingFeeError(StakingError.DomainError("Error estimating fee"))
return
}

View file

@ -4,7 +4,6 @@ import arrow.core.getOrElse
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.staking.*
import com.tangem.domain.staking.model.SubmitHashData
import com.tangem.domain.staking.model.stakekit.*
@ -19,11 +18,7 @@ import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.staking.impl.analytics.StakingAnalyticsEvents
import com.tangem.features.staking.impl.presentation.state.FeeState
import com.tangem.features.staking.impl.presentation.state.StakingStateController
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount
import com.tangem.features.staking.impl.presentation.state.utils.isComposePendingActions
import com.tangem.utils.extensions.orZero
@ -46,7 +41,6 @@ internal class StakingTransactionSender @AssistedInject constructor(
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val submitHashUseCase: SubmitHashUseCase,
private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
@Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus,
@Assisted private val userWallet: UserWallet,
@Assisted private val yield: Yield,
@ -84,7 +78,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
)
if (fullTransactionsData.isNullOrEmpty()) {
onConstructError(StakingError.UnknownError())
onConstructError(StakingError.DomainError("fullTransactionsData is null or empty"))
return
}
@ -193,12 +187,6 @@ internal class StakingTransactionSender @AssistedInject constructor(
type = action?.type,
),
).getOrElse {
analyticsEventHandler.send(
StakingAnalyticsEvents.StakekitError(
token = state.cryptoCurrencySymbol,
stakeKitError = it,
),
)
onConstructError(it)
return emptyList()
}
@ -243,13 +231,7 @@ internal class StakingTransactionSender @AssistedInject constructor(
transactionHash = transactionHash,
),
)
.onLeft { error ->
analyticsEventHandler.send(
StakingAnalyticsEvents.StakekitError(
token = stateController.value.cryptoCurrencySymbol,
stakeKitError = error,
),
)
.onLeft {
saveUnsubmittedHashUseCase.invoke(
transactionId = transaction.id,
transactionHash = transactionHash,

View file

@ -32,14 +32,8 @@ internal class AddStakingErrorTransformer(
}
private fun convertToNotification(error: StakingError): NotificationUM {
return when (error) {
is StakingError.StakedPositionNotFoundError -> StakingNotification.Error.StakedPositionNotFoundError(
message = error.toString(),
)
// TODO staking
else -> StakingNotification.Error.Common(
subtitle = stringReference(error.toString()),
)
}
return StakingNotification.Error.Common(
subtitle = stringReference(error.toString()),
)
}
}

View file

@ -15,6 +15,7 @@ import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.api.ParamsInterceptorHolder
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
@ -46,8 +47,9 @@ import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.staking.impl.analytics.StakeScreenSource
import com.tangem.features.staking.impl.analytics.StakingAnalyticsEvents
import com.tangem.domain.staking.analytics.StakeScreenSource
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
import com.tangem.features.staking.impl.analytics.StakingParamsInterceptor
import com.tangem.features.staking.impl.analytics.utils.StakingAnalyticSender
import com.tangem.features.staking.impl.navigation.InnerStakingRouter
import com.tangem.features.staking.impl.presentation.state.*
@ -73,6 +75,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import java.util.concurrent.CopyOnWriteArrayList
import javax.inject.Inject
@ -107,6 +110,7 @@ internal class StakingViewModel @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getActionsUseCase: GetActionsUseCase,
private val paramsInterceptorHolder: ParamsInterceptorHolder,
@DelayedWork private val coroutineScope: CoroutineScope,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents {
@ -205,6 +209,7 @@ internal class StakingViewModel @Inject constructor(
override fun onCleared() {
super.onCleared()
paramsInterceptorHolder.removeParamsInterceptor(StakingParamsInterceptor.ID)
approvalJobHolder.cancel()
feeJobHolder.cancel()
sendTransactionJobHolder.cancel()
@ -239,18 +244,12 @@ internal class StakingViewModel @Inject constructor(
)
updateNotifications()
},
onStakingFeeError = { error ->
analyticsEventHandler.send(
StakingAnalyticsEvents.StakekitError(
value.cryptoCurrencySymbol,
error,
),
)
onStakingFeeError = {
stateController.update(AddStakingErrorTransformer())
updateNotifications(GetFeeError.UnknownError)
},
onFeeError = { error ->
analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencySymbol))
analyticsEventHandler.send(StakingAnalyticsEvent.TransactionError)
stateController.update(AddStakingErrorTransformer())
updateNotifications(error)
},
@ -278,12 +277,6 @@ internal class StakingViewModel @Inject constructor(
transactionsInProgress.addAll(constructedTransactions)
},
onConstructError = { error ->
analyticsEventHandler.send(
StakingAnalyticsEvents.StakekitError(
token = value.cryptoCurrencySymbol,
stakeKitError = error,
),
)
stakingEventFactory.createStakingErrorAlert(error)
stateController.update(SetConfirmationStateResetAssentTransformer)
},
@ -293,7 +286,7 @@ internal class StakingViewModel @Inject constructor(
stateController.update(SetConfirmationStateCompletedTransformer(txUrl))
},
onSendError = { error ->
analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencySymbol))
analyticsEventHandler.send(StakingAnalyticsEvent.TransactionError)
stakingEventFactory.createSendTransactionErrorAlert(error)
stateController.update(SetConfirmationStateResetAssentTransformer)
},
@ -339,7 +332,7 @@ internal class StakingViewModel @Inject constructor(
}
override fun onInitialInfoBannerClick() {
analyticsEventHandler.send(StakingAnalyticsEvents.WhatIsStaking(value.cryptoCurrencySymbol))
analyticsEventHandler.send(StakingAnalyticsEvent.WhatIsStaking)
innerRouter.openUrl(WHAT_IS_STAKING_ARTICLE_URL)
}
@ -381,22 +374,21 @@ internal class StakingViewModel @Inject constructor(
}
override fun onMaxValueClick() {
analyticsEventHandler.send(StakingAnalyticsEvents.ButtonMax(value.cryptoCurrencySymbol))
analyticsEventHandler.send(StakingAnalyticsEvent.ButtonMax)
stateController.update(AmountMaxValueStateTransformer(cryptoCurrencyStatus, yield))
}
override fun onCurrencyChangeClick(isFiat: Boolean) {
analyticsEventHandler.send(
StakingAnalyticsEvents.AmountSelectCurrency(value.cryptoCurrencySymbol, isFiat),
StakingAnalyticsEvent.AmountSelectCurrency(isFiat),
)
stateController.update(AmountCurrencyChangeStateTransformer(cryptoCurrencyStatus, isFiat))
}
override fun openValidators() {
analyticsEventHandler.send(
StakingAnalyticsEvents.ButtonValidator(
StakingAnalyticsEvent.ButtonValidator(
source = StakeScreenSource.Confirmation,
token = value.cryptoCurrencySymbol,
),
)
stakingStateRouter.showValidators()
@ -404,9 +396,8 @@ internal class StakingViewModel @Inject constructor(
override fun onValidatorSelect(validator: Yield.Validator) {
analyticsEventHandler.send(
StakingAnalyticsEvents.ValidatorChosen(
value.cryptoCurrencySymbol,
validator.name,
StakingAnalyticsEvent.ValidatorChosen(
validator = validator.name,
),
)
stateController.update(
@ -418,9 +409,7 @@ internal class StakingViewModel @Inject constructor(
}
override fun openRewardsValidators() {
analyticsEventHandler.send(
StakingAnalyticsEvents.ButtonRewards(value.cryptoCurrencySymbol),
)
analyticsEventHandler.send(StakingAnalyticsEvent.ButtonRewards)
val rewardsValidators =
stateController.value.rewardsValidatorsState as? StakingStates.RewardsValidatorsState.Data
val rewards = rewardsValidators?.rewards
@ -428,9 +417,8 @@ internal class StakingViewModel @Inject constructor(
onActiveStake(rewards.first())
} else {
analyticsEventHandler.send(
StakingAnalyticsEvents.ButtonValidator(
StakingAnalyticsEvent.ButtonValidator(
source = StakeScreenSource.Info,
token = value.cryptoCurrencySymbol,
),
)
stateController.update {
@ -470,9 +458,8 @@ internal class StakingViewModel @Inject constructor(
override fun onActiveStakeAnalytic() {
analyticsEventHandler.send(
StakingAnalyticsEvents.ButtonValidator(
StakingAnalyticsEvent.ButtonValidator(
source = StakeScreenSource.Info,
token = value.cryptoCurrencySymbol,
),
)
}
@ -522,7 +509,8 @@ internal class StakingViewModel @Inject constructor(
userWalletId = userWalletId,
).fold(
ifLeft = { error ->
analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencySymbol))
Timber.e(error.toString())
analyticsEventHandler.send(StakingAnalyticsEvent.TransactionError)
stateController.update(
SetConfirmationStateAssentApprovalTransformer(
appCurrencyProvider = Provider { appCurrency },
@ -543,7 +531,8 @@ internal class StakingViewModel @Inject constructor(
network = tokenCryptoCurrency.network,
).fold(
ifLeft = { error ->
analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencySymbol))
Timber.e(error.toString())
analyticsEventHandler.send(StakingAnalyticsEvent.TransactionError)
stateController.update(
SetConfirmationStateAssentApprovalTransformer(
appCurrencyProvider = Provider { appCurrency },
@ -673,7 +662,7 @@ internal class StakingViewModel @Inject constructor(
}
override fun onExploreClick() {
analyticsEventHandler.send(StakingAnalyticsEvents.ButtonExplore)
analyticsEventHandler.send(StakingAnalyticsEvent.ButtonExplore)
val confirmationDataState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data
val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content
val txUrl = transactionDoneState?.txUrl
@ -687,7 +676,7 @@ internal class StakingViewModel @Inject constructor(
val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content
val txUrl = transactionDoneState?.txUrl
analyticsEventHandler.send(StakingAnalyticsEvents.ButtonShare)
analyticsEventHandler.send(StakingAnalyticsEvent.ButtonShare)
if (txUrl != null) {
vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click)
stakingEventFactory.createShareDialog(txUrl = txUrl)
@ -769,10 +758,12 @@ internal class StakingViewModel @Inject constructor(
if (!isInitialInfoAnalyticSent) {
isInitialInfoAnalyticSent = true
val balances = status.value.yieldBalance as? YieldBalance.Data
paramsInterceptorHolder.addParamsInterceptor(
interceptor = StakingParamsInterceptor(status.currency.symbol),
)
analyticsEventHandler.send(
StakingAnalyticsEvents.StakingInfoScreenOpened(
StakingAnalyticsEvent.StakingInfoScreenOpened(
validatorsCount = balances?.getValidatorsCount() ?: 0,
token = status.currency.symbol,
),
)
}

View file

@ -88,7 +88,7 @@ markdownComposeView = "0.5.4"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "develop-836"
tangemBlockchainSdk = "develop-841"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-391"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^