Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-24 10:50:03 +01:00
commit bc84d8f5f8
579 changed files with 13604 additions and 3422 deletions

View file

@ -1,5 +1,6 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.kotlin.serialization)
id("configuration")
}
@ -7,4 +8,6 @@ dependencies {
api(deps.kotlin.coroutines)
api(deps.arrow.core)
api(deps.arrow.fx)
implementation(deps.kotlin.serialization)
}

View file

@ -96,4 +96,15 @@ sealed class Lce<out E : Any, out C : Any> {
ifContent = ::identity,
ifError = { null },
)
/**
* Returns the error of this [Lce] if it's a [Lce.Error], `null` otherwise.
*
* @return The error of this [Lce] or `null`.
*/
fun errorOrNull(): E? = fold(
ifLoading = { null },
ifContent = { null },
ifError = ::identity,
)
}

View file

@ -2,8 +2,10 @@ package com.tangem.domain.core.lce
import arrow.atomic.Atomic
import arrow.core.raise.Raise
import arrow.core.raise.RaiseDSL
import arrow.core.raise.recover
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import kotlin.experimental.ExperimentalTypeInference
@ -18,18 +20,55 @@ class LceRaise<E : Any> @PublishedApi internal constructor(
private val raise: Raise<Lce<E, Nothing>>,
) : Raise<Lce<E, Nothing>> by raise {
/**
* An [Atomic] boolean flag indicating whether a loading operation is in progress.
*/
val isLoading: Atomic<Boolean> = Atomic(false)
/**
* Helper function to raise an [Lce.Error] state with the given error object.
* */
@RaiseDSL
@JvmName(name = "raiseError")
fun raise(r: E): Nothing = raise(r = r.lceError())
/**
* Helper function to raise an [Lce.Loading] state.
*/
@RaiseDSL
fun raiseLoading(): Nothing = raise(r = lceLoading())
/**
* Execute the [Raise] context function resulting in [C] or any _logical error_ of type [OtherError],
* and transform any raised [OtherError] into [E], which is raised to the outer [Raise].
*
* @see arrow.core.raise.withError
* */
@RaiseDSL
@OptIn(ExperimentalTypeInference::class)
inline fun <OtherError : Any, C : Any> withError(
transform: (OtherError) -> E,
@BuilderInference block: LceRaise<OtherError>.() -> C,
): C = recover(
block = { block(LceRaise(raise = this@recover)) },
recover = { error ->
error.fold<Nothing>(
ifLoading = { raiseLoading() },
ifError = { raise(transform(it)) },
ifContent = { it },
)
},
)
/**
* Binds the content of this [Lce] instance and handles its state.
* If this is a [Lce.Loading] state, sets the [isLoading] flag to true and calls the [ifLoading] function.
* If this is a [Lce.Content] state, returns the content.
* If this is a [Lce.Error] state, raises the error.
*
* @param ifLoading The function to call if this is a [Lce.Loading] state.
* By default, it raises a new [Lce.Loading] state.
* @return The content of this [Lce] instance.
*/
@RaiseDSL
fun <C : Any> Lce<E, C>.bind(): C = when (this) {
is Lce.Loading -> {
isLoading.set(true)
@ -48,6 +87,7 @@ class LceRaise<E : Any> @PublishedApi internal constructor(
*
* @return The content of this [Lce] instance.
*/
@RaiseDSL
fun <C : Any> Lce<E, C>.bindOrNull(): C? = when (this) {
is Lce.Loading -> {
isLoading.set(true)

View file

@ -0,0 +1,21 @@
package com.tangem.domain.core.serialization
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.math.BigDecimal
internal object BigDecimalSerializer : KSerializer<BigDecimal> {
override val descriptor = PrimitiveSerialDescriptor(serialName = "BigDecimal", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: BigDecimal) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): BigDecimal {
return BigDecimal(decoder.decodeString())
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.domain.core.serialization
import kotlinx.serialization.Serializable
import java.math.BigDecimal
typealias SerializedBigDecimal = @Serializable(with = BigDecimalSerializer::class) BigDecimal

View file

@ -19,6 +19,6 @@ object NetworkLogConfig {
}
object AnalyticsHandlersLogConfig {
const val firebase: Boolean = false
val firebase: Boolean = BuildConfig.LOG_ENABLED
val amplitude: Boolean = BuildConfig.LOG_ENABLED
}

View file

@ -11,5 +11,5 @@ interface ReduxStateHolder {
suspend fun onUserWalletSelected(userWallet: UserWallet)
fun sendFeedbackEmail()
fun dispatchDialogShow(dialog: StateDialog)
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.redux
interface StateDialog {
data class ScanFailsDialog(val source: ScanFailsSource, val onTryAgain: (() -> Unit)? = null) : StateDialog
enum class ScanFailsSource {
MAIN, SIGN_IN, SETTINGS, INTRO;
}
}

View file

@ -45,7 +45,7 @@ import timber.log.Timber
import java.math.BigDecimal
import java.util.EnumSet
@Suppress("LargeClass", "TooManyFunctions", "LongParameterList")
@Suppress("LargeClass", "TooManyFunctions")
// FIXME: Move to its own module and make internal
@Deprecated("Inject the WalletManagerFacade interface using DI instead")
class DefaultWalletManagersFacade(
@ -470,7 +470,7 @@ class DefaultWalletManagersFacade(
amount: Amount,
userWalletId: UserWalletId,
network: Network,
): Result<TransactionFee>? {
): Result<TransactionFee>? = withContext(dispatchers.io) {
val blockchain = Blockchain.fromId(network.id.value)
val walletManager = getOrCreateWalletManager(
userWalletId = userWalletId,
@ -480,7 +480,7 @@ class DefaultWalletManagersFacade(
val destination = estimationFeeAddressFactory.makeAddress(blockchain)
return (walletManager as? TransactionSender)?.estimateFee(
(walletManager as? TransactionSender)?.estimateFee(
amount = amount,
destination = destination,
)

View file

@ -0,0 +1,13 @@
package com.tangem.domain.settings
import arrow.core.Either
import com.tangem.domain.settings.repositories.PermissionRepository
class DelayPermissionRequestUseCase(
private val repository: PermissionRepository,
) {
suspend operator fun invoke(permission: String): Either<Throwable, Unit> = Either.catch {
repository.delayPermissionAsking(permission)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.domain.settings
import arrow.core.Either
import com.tangem.domain.settings.repositories.PermissionRepository
class IsFirstTimeAskingPermissionUseCase(private val repository: PermissionRepository) {
suspend operator fun invoke(permission: String): Either<Throwable, Boolean> = Either.catch {
repository.isFirstTimeAskingPermission(permission)
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.domain.settings
import arrow.core.Either
import com.tangem.domain.settings.repositories.PermissionRepository
class NeverRequestPermissionUseCase(
private val repository: PermissionRepository,
) {
suspend operator fun invoke(permission: String): Either<Throwable, Unit> = Either.catch {
repository.neverAskPermission(permission)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.domain.settings
import arrow.core.Either
import com.tangem.domain.settings.repositories.PermissionRepository
class SetFirstTimeAskingPermissionUseCase(private val repository: PermissionRepository) {
suspend operator fun invoke(permission: String): Either<Throwable, Unit> = Either.catch {
repository.setFirstTimeAskingPermission(permission, false)
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.settings
import com.tangem.domain.settings.repositories.PermissionRepository
class ShouldAskPermissionUseCase(
private val repository: PermissionRepository,
) {
suspend operator fun invoke(permission: String): Boolean = repository.shouldAskPermission(permission)
}

View file

@ -0,0 +1,11 @@
package com.tangem.domain.settings
import arrow.core.Either
import com.tangem.domain.settings.repositories.PermissionRepository
class ShouldInitiallyAskPermissionUseCase(private val repository: PermissionRepository) {
suspend operator fun invoke(permission: String): Either<Throwable, Boolean> = Either.catch {
repository.shouldInitiallyShowPermissionScreen(permission)
}
}

View file

@ -0,0 +1,39 @@
package com.tangem.domain.settings.repositories
interface PermissionRepository {
/**
* Return true if should display screen ONCE asking to allow [permission].
* False otherwise or screen already was displayed
*/
suspend fun shouldInitiallyShowPermissionScreen(permission: String): Boolean
/**
* Indicates which time [permission] was asked via platform dialog.
* NOTE: Use this method to indicate either reroute to settings or display platform dialog.
*/
suspend fun isFirstTimeAskingPermission(permission: String): Boolean
/**
* Sets value indicating that [permission] was asked via platform dialog.
* NOTE: Use this method to indicate either reroute to settings or display platform dialog.
*/
suspend fun setFirstTimeAskingPermission(permission: String, value: Boolean)
/**
* Is clear to ask [permission].
* User could already granted or permanently denied permission
* Or there is an active delay before next request
*/
suspend fun shouldAskPermission(permission: String): Boolean
/**
* Permanently deny [permission] and never request again
*/
suspend fun neverAskPermission(permission: String)
/**
* Delay next [permission] request for some time or active sessions
*/
suspend fun delayPermissionAsking(permission: String)
}

View file

@ -1,9 +1,20 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.serialization)
id("configuration")
}
android {
namespace = "com.tangem.domain.staking"
}
dependencies {
implementation(deps.kotlin.coroutines)
implementation(deps.arrow.core)
api(projects.domain.staking.models)
api(projects.domain.core)
implementation(deps.kotlin.serialization)
implementation(projects.domain.tokens.models)
}

1
domain/staking/models/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,12 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.kotlin.serialization)
id("configuration")
}
dependencies {
implementation(projects.domain.core)
implementation(deps.kotlin.serialization)
implementation(deps.jodatime)
}

View file

@ -0,0 +1,71 @@
package com.tangem.domain.staking.model
enum class NetworkType {
AVALANCHE_C,
AVALANCHE_ATOMIC,
AVALANCHE_P,
ARBITRUM,
BINANCE,
CELO,
ETHEREUM,
ETHEREUM_GOERLI,
ETHEREUM_HOLESKY,
FANTOM,
HARMONY,
OPTIMISM,
POLYGON,
GNOSIS,
MOONRIVER,
OKC,
ZKSYNC,
VICTION,
AGORIC,
AKASH,
AXELAR,
BAND_PROTOCOL,
BITSONG,
CANTO,
CHIHUAHUA,
COMDEX,
COREUM,
COSMOS,
CRESCENT,
CRONOS,
CUDOS,
DESMOS,
DYDX,
EVMOS,
FETCH_AI,
GRAVITY_BRIDGE,
INJECTIVE,
IRISNET,
JUNO,
KAVA,
KI_NETWORK,
MARS_PROTOCOL,
NYM,
OKEX_CHAIN,
ONOMY,
OSMOSIS,
PERSISTENCE,
QUICKSILVER,
REGEN,
SECRET,
SENTINEL,
SOMMELIER,
STAFI,
STARGAZE,
STRIDE,
TERITORI,
TGRADE,
UMEE,
POLKADOT,
KUSAMA,
WESTEND,
BINANCEBEACON,
NEAR,
SOLANA,
TEZOS,
TRON,
UNKNOWN,
}

View file

@ -0,0 +1,118 @@
package com.tangem.domain.staking.model
import com.tangem.domain.core.serialization.SerializedBigDecimal
import kotlinx.serialization.Serializable
@Serializable
data class Yield(
val id: String,
val token: Token,
val tokens: List<Token>,
val args: Args,
val status: Status,
val apy: SerializedBigDecimal,
val rewardRate: Double,
val rewardType: RewardType,
val metadata: Metadata,
val validators: List<Validator>,
val isAvailable: Boolean,
) {
@Serializable
data class Status(
val enter: Boolean,
val exit: Boolean?,
)
@Serializable
data class Args(
val enter: Enter,
val exit: Enter?,
) {
@Serializable
data class Enter(
val addresses: Addresses,
val args: Map<String, AddressArgument>,
) {
@Serializable
data class Addresses(
val address: AddressArgument,
val additionalAddresses: Map<String, AddressArgument>? = null,
)
}
}
@Serializable
data class Validator(
val address: String,
val status: String,
val name: String,
val image: String?,
val website: String?,
val apr: SerializedBigDecimal?,
val commission: Double?,
val stakedBalance: String?,
val votingPower: Double?,
val preferred: Boolean,
)
@Serializable
data class Metadata(
val name: String,
val logoUri: String,
val description: String,
val documentation: String?,
val gasFeeToken: Token,
val token: Token,
val tokens: List<Token>,
val type: String,
val rewardSchedule: String,
val cooldownPeriod: Period,
val warmupPeriod: Period,
val rewardClaiming: String,
val defaultValidator: String?,
val minimumStake: Int?,
val supportsMultipleValidators: Boolean,
val revshare: Enabled,
val fee: Enabled,
) {
@Serializable
data class Period(
val days: Int,
)
@Serializable
data class Enabled(
val enabled: Boolean,
)
}
enum class RewardType {
APY, // compound rate
APR, // simple rate
UNKNOWN,
}
}
@Serializable
data class Token(
val name: String,
val network: NetworkType,
val symbol: String,
val decimals: Int,
val address: String?,
val coinGeckoId: String?,
val logoURI: String?,
val isPoints: Boolean?,
)
@Serializable
data class AddressArgument(
val required: Boolean,
val network: String? = null,
val minimum: Double? = null,
val maximum: Double? = null,
)

View file

@ -0,0 +1,18 @@
package com.tangem.domain.staking.model.action
import com.tangem.domain.staking.model.transaction.StakingTransaction
import org.joda.time.DateTime
import java.math.BigDecimal
data class EnterAction(
val id: String,
val integrationId: String,
val status: StakingActionStatus,
val type: StakingActionType,
val currentStepIndex: Int,
val amount: BigDecimal,
val validatorAddress: String?,
val validatorAddresses: List<String>?,
val transactions: List<StakingTransaction>?,
val createdAt: DateTime,
)

View file

@ -0,0 +1,11 @@
package com.tangem.domain.staking.model.action
enum class StakingActionStatus {
CANCELED,
CREATED,
WAITING_FOR_NEXT,
PROCESSING,
FAILED,
SUCCESS,
UNKNOWN,
}

View file

@ -0,0 +1,20 @@
package com.tangem.domain.staking.model.action
enum class StakingActionType {
STAKE,
UNSTAKE,
CLAIM_REWARDS,
RESTAKE_REWARDS,
WITHDRAW,
RESTAKE,
CLAIM_UNSTAKED,
UNLOCK_LOCKED,
STAKE_LOCKED,
VOTE,
REVOKE,
VOTE_LOCKED,
REVOTE,
REBOND,
MIGRATE,
UNKNOWN,
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.staking.model.transaction
import com.tangem.domain.staking.model.Token
import java.math.BigDecimal
data class StakingGasEstimate(
val amount: BigDecimal,
val token: Token,
val gasLimit: BigDecimal,
)

View file

@ -0,0 +1,20 @@
package com.tangem.domain.staking.model.transaction
import com.tangem.domain.staking.model.NetworkType
data class StakingTransaction(
val id: String,
val network: NetworkType,
val status: StakingTransactionStatus,
val type: StakingTransactionType,
val hash: String?,
val signedTransaction: String?,
val unsignedTransaction: String?,
val stepIndex: Int,
val error: String?,
val gasEstimate: StakingGasEstimate?,
val stakeId: String?,
val explorerUrl: String?,
val ledgerHwAppId: String?,
val isMessage: Boolean,
)

View file

@ -0,0 +1,15 @@
package com.tangem.domain.staking.model.transaction
enum class StakingTransactionStatus {
NOT_FOUND,
CREATED,
BLOCKED,
WAITING_FOR_SIGNATURE,
SIGNED,
BROADCASTED,
PENDING,
CONFIRMED,
FAILED,
SKIPPED,
UNKNOWN,
}

View file

@ -0,0 +1,44 @@
package com.tangem.domain.staking.model.transaction
enum class StakingTransactionType {
SWAP,
DEPOSIT,
APPROVAL,
STAKE,
CLAIM_UNSTAKED,
CLAIM_REWARDS,
RESTAKE_REWARDS,
UNSTAKE,
SPLIT,
MERGE,
LOCK,
UNLOCK,
SUPPLY,
BRIDGE,
VOTE,
REVOKE,
RESTAKE,
REBOND,
WITHDRAW,
CREATE_ACCOUNT,
REVEAL,
MIGRATE,
UTXO_P_TO_C_IMPORT,
UTXO_C_TO_P_IMPORT,
UNFREEZE_LEGACY,
UNFREEZE_LEGACY_BANDWIDTH,
UNFREEZE_LEGACY_ENERGY,
UNFREEZE_BANDWIDTH,
UNFREEZE_ENERGY,
FREEZE_BANDWIDTH,
FREEZE_ENERGY,
UNDELEGATE_BANDWIDTH,
UNDELEGATE_ENERGY,
P2P_NODE_REQUEST,
LUGANODES_PROVISION,
LUGANODES_EXIT_REQUEST,
INFSTONES_PROVISION,
INFSTONES_EXIT_REQUEST,
INFSTONES_CLAIM_REQUEST,
UNKNOWN,
}

View file

@ -0,0 +1,23 @@
package com.tangem.domain.staking
import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.staking.error.StakingTokensError
import com.tangem.domain.staking.repositories.StakingRepository
/**
* Use case for getting enabled tokens
*/
class FetchStakingTokensUseCase(
private val stakingRepository: StakingRepository,
) {
suspend operator fun invoke(): Either<Throwable, Unit> {
return either {
catch(
block = { stakingRepository.fetchEnabledYields() },
catch = { StakingTokensError.DataError(it) },
)
}
}
}

View file

@ -2,15 +2,16 @@ package com.tangem.domain.staking
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrency
/**
* Use case for getting info about staking availability for certain blockchain.
* Use case for getting info about staking capability in tangem app.
*/
class GetStakingAvailabilityUseCase(
private val stakingRepository: StakingRepository,
) {
operator fun invoke(blockchainNetworkId: String): StakingAvailability {
return stakingRepository.getStakingAvailability(blockchainNetworkId)
suspend operator fun invoke(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingAvailability {
return stakingRepository.getStakingAvailabilityForActions(cryptoCurrencyId, symbol)
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.domain.staking
import arrow.core.Either
import com.tangem.domain.staking.model.Yield
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrency
/**
* Use case for getting staking yield for staking scenario start.
*/
class GetYieldUseCase(private val stakingRepository: StakingRepository) {
suspend operator fun invoke(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Either<Throwable, Yield> {
return Either.catch { stakingRepository.getYield(cryptoCurrencyId, symbol) }
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.domain.staking.error
sealed class StakingTokensError {
data class DataError(val cause: Throwable) : StakingTokensError()
}

View file

@ -5,4 +5,6 @@ sealed class StakingAvailability {
data class Available(val integrationId: String) : StakingAvailability()
data object Unavailable : StakingAvailability()
data object TemporaryDisabled : StakingAvailability()
}

View file

@ -0,0 +1,9 @@
package com.tangem.domain.staking.model
data class StakingToken(
val name: String,
val symbol: String,
val decimals: Int,
val contractAddress: String?,
val coinGeckoId: String?,
)

View file

@ -0,0 +1,6 @@
package com.tangem.domain.staking.model
data class StakingTokenWithYield(
val token: StakingToken,
val availableYieldIds: List<String>,
)

View file

@ -2,10 +2,21 @@ package com.tangem.domain.staking.repositories
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.Yield
import com.tangem.domain.tokens.model.CryptoCurrency
interface StakingRepository {
fun getStakingAvailability(blockchainId: String): StakingAvailability
fun isStakingSupported(currencyId: String): Boolean
suspend fun fetchEnabledYields()
suspend fun getEntryInfo(integrationId: String): StakingEntryInfo
suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield
suspend fun getStakingAvailabilityForActions(
cryptoCurrencyId: CryptoCurrency.ID,
symbol: String,
): StakingAvailability
}

View file

@ -14,6 +14,7 @@ dependencies {
api(projects.domain.core)
implementation(projects.domain.models)
implementation(projects.domain.legacy)
implementation(projects.domain.staking)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.tokens.models)
implementation(projects.domain.txhistory.models)

View file

@ -1,7 +1,7 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("kotlin-parcelize")
alias(deps.plugins.kotlin.serialization)
id("configuration")
}
@ -10,6 +10,7 @@ android {
}
dependencies {
implementation(deps.kotlin.serialization)
implementation(projects.domain.txhistory.models)
implementation(projects.core.analytics.models)
implementation(deps.tangem.blockchain) {

View file

@ -1,7 +1,6 @@
package com.tangem.domain.tokens.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
/**
* Represents a generic cryptocurrency.
@ -14,8 +13,8 @@ import kotlinx.parcelize.Parcelize
* @property iconUrl Optional URL of the cryptocurrency icon. `null` if not found.
* @property isCustom Indicates whether the currency is a custom user-added currency or not.
*/
@Parcelize
sealed class CryptoCurrency : Parcelable {
@Serializable
sealed class CryptoCurrency {
abstract val id: ID
abstract val network: Network
@ -28,6 +27,7 @@ sealed class CryptoCurrency : Parcelable {
/**
* Represents a native coin in the blockchain network.
*/
@Serializable
data class Coin(
override val id: ID,
override val network: Network,
@ -48,6 +48,7 @@ sealed class CryptoCurrency : Parcelable {
*
* @property contractAddress Address of the contract managing the token.
*/
@Serializable
data class Token(
override val id: ID,
override val network: Network,
@ -75,12 +76,12 @@ sealed class CryptoCurrency : Parcelable {
* @property rawCurrencyId Represents not unique currency ID from the blockchain network. `null` if
* its ID of the custom token.
*/
@Parcelize
@Serializable
data class ID(
private val prefix: Prefix,
private val body: Body,
private val suffix: Suffix,
) : Parcelable {
) {
val value: String
get() = buildString {
@ -121,12 +122,13 @@ sealed class CryptoCurrency : Parcelable {
*
* The body can be either a raw network ID or a raw network ID with a network derivation path.
*/
@Parcelize
sealed class Body : Parcelable {
@Serializable
sealed class Body {
/** The value of the body. */
abstract val value: String
@Serializable
/** Represents a raw network ID. */
data class NetworkId(val rawId: String) : Body() {
override val value: String get() = rawId
@ -137,6 +139,7 @@ sealed class CryptoCurrency : Parcelable {
*
* Should be used for a cryptocurrencies with custom derivation path.
* */
@Serializable
data class NetworkIdWithDerivationPath(
val rawId: String,
val derivationPath: String,
@ -155,13 +158,14 @@ sealed class CryptoCurrency : Parcelable {
*
* The suffix can either be a raw ID or a contract address.
*/
@Parcelize
sealed class Suffix : Parcelable {
@Serializable
sealed class Suffix {
/** The value of the suffix, which could be either a raw ID or a contract address. */
abstract val value: String
/** Represents a raw ID suffix. */
@Serializable
data class RawID(val rawId: String, val contractAddress: String? = null) : Suffix() {
override val value: String
get() = buildString {
@ -174,6 +178,7 @@ sealed class CryptoCurrency : Parcelable {
}
/** Represents a contract address suffix. */
@Serializable
data class ContractAddress(val contractAddress: String) : Suffix() {
override val value: String get() = contractAddress
}
@ -183,11 +188,11 @@ sealed class CryptoCurrency : Parcelable {
return "ID(value='$value')"
}
private companion object {
companion object {
// should use delimiters that could be used in URL not like path or query delimiters
const val PREFIX_DELIMITER = '_'
const val SUFFIX_DELIMITER = ';'
const val DERIVATION_PATH_DELIMITER = 'd'
private const val PREFIX_DELIMITER = '_'
private const val SUFFIX_DELIMITER = ';'
private const val DERIVATION_PATH_DELIMITER = 'd'
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.domain.tokens.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
/**
* Represents a blockchain network, identified by a unique ID, a human-readable name, and its standard type.
@ -20,7 +19,7 @@ import kotlinx.parcelize.Parcelize
* that cannot be represented in a fiat currency.
* (For those blockchains that have FeeResource instead of a standard type of fee)
*/
@Parcelize
@Serializable
data class Network(
val id: ID,
val backendId: String,
@ -30,7 +29,7 @@ data class Network(
val isTestnet: Boolean,
val standardType: StandardType,
val hasFiatFeeRate: Boolean,
) : Parcelable {
) {
init {
require(name.isNotBlank()) { "Network name must not be blank" }
@ -42,8 +41,8 @@ data class Network(
* @property value The string representation of the network ID.
*/
@JvmInline
@Parcelize
value class ID(val value: String) : Parcelable {
@Serializable
value class ID(val value: String) {
init {
require(value.isNotBlank()) { "Network ID must not be blank" }
@ -56,8 +55,8 @@ data class Network(
* This class represents such paths in a generic manner, allowing for predefined card-based paths,
* custom paths, or even no derivation path at all.
*/
@Parcelize
sealed class DerivationPath : Parcelable {
@Serializable
sealed class DerivationPath {
/** The actual derivation path value, if any. */
abstract val value: String?
@ -67,6 +66,7 @@ data class Network(
*
* @property value The derivation path string.
*/
@Serializable
data class Card(override val value: String) : DerivationPath()
/**
@ -74,12 +74,14 @@ data class Network(
*
* @property value The derivation path string.
*/
@Serializable
data class Custom(override val value: String) : DerivationPath()
/**
* Represents a lack of derivation path.
*/
object None : DerivationPath() {
@Serializable
data object None : DerivationPath() {
override val value: String? get() = null
}
}
@ -93,31 +95,36 @@ data class Network(
*
* @property name The human-readable name of the standard type.
*/
@Parcelize
sealed class StandardType : Parcelable {
@Serializable
sealed class StandardType {
abstract val name: String
/** Represents the ERC20 token standard, common on the Ethereum network. */
object ERC20 : StandardType() {
@Serializable
data object ERC20 : StandardType() {
override val name: String get() = "ERC20"
}
/** Represents the TRC20 token standard, common on the TRON network. */
object TRC20 : StandardType() {
@Serializable
data object TRC20 : StandardType() {
override val name: String get() = "TRC20"
}
/** Represents the BEP20 token standard, common on the Binance Smart Chain network. */
object BEP20 : StandardType() {
@Serializable
data object BEP20 : StandardType() {
override val name: String get() = "BEP20"
}
/** Represents the BEP2 token standard, common on the Binance Chain network. */
object BEP2 : StandardType() {
@Serializable
data object BEP2 : StandardType() {
override val name: String get() = "BEP2"
}
/** Represents a network that does not adhere to a predefined standard type. */
@Serializable
data class Unspecified(override val name: String) : StandardType()
}
}

View file

@ -2,6 +2,8 @@ package com.tangem.domain.tokens
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -28,6 +30,7 @@ class GetCryptoCurrencyActionsUseCase(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
private val dispatchers: CoroutineDispatcherProvider,
) {
@ -121,6 +124,17 @@ class GetCryptoCurrencyActionsUseCase(
activeList.add(TokenActionsState.ActionState.Receive(scenario))
}
// staking
if (isStakingAvailable(cryptoCurrency)) {
activeList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.None))
} else {
disabledList.add(
TokenActionsState.ActionState.Stake(
unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(cryptoCurrency.name),
),
)
}
// send
val sendUnavailabilityReason = getSendUnavailabilityReason(
cryptoCurrencyStatus = cryptoCurrencyStatus,
@ -234,6 +248,7 @@ class GetCryptoCurrencyActionsUseCase(
}
actionsList.add(TokenActionsState.ActionState.Receive(scenario))
}
actionsList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable))
actionsList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None))
return actionsList
}
@ -246,7 +261,7 @@ class GetCryptoCurrencyActionsUseCase(
cryptoCurrencyStatus.value.amount.isNullOrZero() -> {
ScenarioUnavailabilityReason.EmptyBalance(ScenarioUnavailabilityReason.WithdrawalScenario.SEND)
}
currenciesRepository.hasPendingTransactions(
currenciesRepository.isSendBlockedByPendingTransactions(
cryptoCurrencyStatus = cryptoCurrencyStatus,
coinStatus = coinStatus,
) -> {
@ -264,4 +279,11 @@ class GetCryptoCurrencyActionsUseCase(
private fun isAddressAvailable(networkAddress: NetworkAddress?): Boolean {
return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty()
}
private suspend fun isStakingAvailable(cryptoCurrency: CryptoCurrency): Boolean {
return stakingRepository.getStakingAvailabilityForActions(
cryptoCurrencyId = cryptoCurrency.id,
symbol = cryptoCurrency.symbol,
) is StakingAvailability.Available
}
}

View file

@ -3,6 +3,9 @@ package com.tangem.domain.tokens.model
sealed class ScenarioUnavailabilityReason {
data object None : ScenarioUnavailabilityReason()
// staking-specific
data class StakingUnavailable(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason()
// send&sell-specific
data class PendingTransaction(
val withdrawalScenario: WithdrawalScenario,
@ -24,6 +27,6 @@ sealed class ScenarioUnavailabilityReason {
data object UnassociatedAsset : ScenarioUnavailabilityReason()
enum class WithdrawalScenario {
SELL, SEND
SELL, SEND // TODO staking create&process STAKING
}
}

View file

@ -20,6 +20,8 @@ data class TokenActionsState(
data class Receive(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState()
data class Stake(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState()
data class Swap(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState()
data class Send(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState()

View file

@ -194,12 +194,15 @@ interface CurrenciesRepository {
fun getMissedAddressesCryptoCurrencies(userWalletId: UserWalletId): Flow<List<CryptoCurrency>>
/**
* Determines whether the currency has pending transaction or currency network has pending transaction
* Determines whether the currency sending is blocked by network pending transaction
*
* @param cryptoCurrencyStatus currency status
* @param coinStatus main currency status in [cryptoCurrencyStatus] network
*/
fun hasPendingTransactions(cryptoCurrencyStatus: CryptoCurrencyStatus, coinStatus: CryptoCurrencyStatus?): Boolean
fun isSendBlockedByPendingTransactions(
cryptoCurrencyStatus: CryptoCurrencyStatus,
coinStatus: CryptoCurrencyStatus?,
): Boolean
/**
* Retrieves fee paid currency for specific [currency].

View file

@ -122,7 +122,7 @@ internal class MockCurrenciesRepository(
return isSortedByBalance.map { it.getOrElse { e -> throw e } }
}
override fun hasPendingTransactions(
override fun isSendBlockedByPendingTransactions(
cryptoCurrencyStatus: CryptoCurrencyStatus,
coinStatus: CryptoCurrencyStatus?,
): Boolean {

View file

@ -12,10 +12,8 @@ import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import java.math.BigDecimal
/**
@ -23,7 +21,6 @@ import java.math.BigDecimal
*/
class EstimateFeeUseCase(
private val walletManagersFacade: WalletManagersFacade,
private val dispatcher: CoroutineDispatcherProvider,
) {
suspend operator fun invoke(
amount: BigDecimal,
@ -43,7 +40,7 @@ class EstimateFeeUseCase(
null -> GetFeeError.UnknownError.left()
}
emit(maybeFee)
}.flowOn(dispatcher.io)
}
}
private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount(

View file

@ -16,7 +16,7 @@ class ValidateTransactionUseCase(
@Suppress("LongParameterList")
suspend operator fun invoke(
amount: Amount,
fee: Fee,
fee: Fee?,
memo: String?,
destination: String,
userWalletId: UserWalletId,

View file

@ -1,5 +1,6 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.kotlin.serialization)
id("configuration")
}
@ -11,4 +12,8 @@ dependencies {
// region Domain modules
implementation(project(":domain:models"))
// endregion
// region Other libraries
implementation(deps.kotlin.serialization)
// endregion
}

View file

@ -2,9 +2,10 @@ package com.tangem.domain.wallets.models
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import java.io.Serializable
import kotlinx.serialization.Serializable
data class UserWalletId(val stringValue: String) : Serializable {
@Serializable
data class UserWalletId(val stringValue: String) {
val value = stringValue.hexToBytes()