Updated on 2026-08-14

This commit is contained in:
Tangem 2024-05-27 11:42:24 +01:00
commit c3522320ae
660 changed files with 10672 additions and 6904 deletions

View file

@ -14,6 +14,7 @@ dependencies {
implementation(projects.domain.demo)
implementation(projects.domain.core)
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
// TODO: Remove after new card scan result was implemented
implementation(projects.domain.models)
implementation(projects.domain.tokens.models)

View file

@ -1,15 +0,0 @@
package com.tangem.domain.card
import com.tangem.domain.card.repository.CardSdkConfigRepository
/**
* Use case for getting access code saving status
*
* @property cardSdkConfigRepository repository for managing of CardSDK config
*
[REDACTED_AUTHOR]
*/
class GetAccessCodeSavingStatusUseCase(private val cardSdkConfigRepository: CardSdkConfigRepository) {
operator fun invoke(): Boolean = cardSdkConfigRepository.isAccessCodeSavingEnabled()
}

View file

@ -27,9 +27,6 @@ interface CardSdkConfigRepository {
/** Update the card ID display format according to the [productType] of the scanned card */
fun updateCardIdDisplayFormat(productType: ProductType)
/** Check if access code saving is enabled */
fun isAccessCodeSavingEnabled(): Boolean
/** Get common signer by [cardId] */
fun getCommonSigner(cardId: String?): CommonSigner

View file

@ -0,0 +1,96 @@
package com.tangem.domain.core.lce
import arrow.core.identity
import com.tangem.domain.core.utils.flatMap
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
/**
* A sealed class representing the three states of a data load operation: Loading, Content, and Error.
*
* @param E The type of the error object.
* @param C The type of the content object.
*/
sealed class Lce<out E : Any, out C : Any> {
/**
* Represents the loading state, which may contain partial content.
*
* @param partialContent The partial content that has been loaded so far, if any.
*/
data class Loading<C : Any>(val partialContent: C?) : Lce<Nothing, C>()
/**
* Represents the content state, which contains the loaded content.
*
* @param content The loaded content.
*/
data class Content<C : Any>(val content: C) : Lce<Nothing, C>()
/**
* Represents the error state, which contains an error object.
*
* @param error The error that occurred during loading.
*/
data class Error<E : Any>(val error: E) : Lce<E, Nothing>()
/**
* Applies the given functions to the content, error, or partial content of this Lce, depending on its state.
*
* @param ifLoading The function to apply if this is a [Loading] state.
* @param ifContent The function to apply if this is a [Content] state.
* @param ifError The function to apply if this is an [Error] state.
* @return The result of applying the corresponding function.
*/
inline fun <T> fold(
ifLoading: (partialContent: C?) -> T,
ifContent: (content: C) -> T,
ifError: (error: E) -> T,
): T = when (this) {
is Loading -> ifLoading(partialContent)
is Error -> ifError(error)
is Content -> ifContent(content)
}
/**
* Transforms the content of this [Lce] by applying the given function.
* If this is a [Content] state, the function is applied to the [Content.content].
* If this is a [Loading] state and partialContent is present,
* the function is applied to the [Loading.partialContent].
*
* @param ifContent The function to apply to the content or partial content.
* @return A new [Lce] instance containing the result of applying the function.
*/
inline fun <T : Any> map(ifContent: (C) -> T): Lce<E, T> = flatMap { content, isLoading ->
if (isLoading) {
lceLoading(ifContent(content))
} else {
ifContent(content).lceContent()
}
}
/**
* Transforms the error of this [Lce] by applying the given function, if this is an [Error] state.
*
* @param ifError The function to apply to the error.
* @return A new [Lce] with the transformed error, or this [Lce] unchanged if it is not an [Error] state.
*/
inline fun <T : Any> mapError(ifError: (E) -> T): Lce<T, C> = when (this) {
is Loading -> this
is Content -> this
is Error -> ifError(error).lceError()
}
/**
* Returns the content of this [Lce] if it's a [Lce.Content] or partial content if it's a [Lce.Loading],
* `null` if it's a [Lce.Error].
*
* @return The content of this [Lce] or `null`.
*/
fun getOrNull(): C? = fold(
ifLoading = ::identity,
ifContent = ::identity,
ifError = { null },
)
}

View file

@ -0,0 +1,99 @@
package com.tangem.domain.core.lce
import arrow.core.raise.Raise
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceLoading
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.launch
import kotlin.experimental.ExperimentalTypeInference
/**
* A [Flow] of [Lce]
*
* @param E The type of the error object.
* @param C The type of the content object.
* */
typealias LceFlow<E, C> = Flow<Lce<E, C>>
/**
* A class that wraps a [LceRaise] instance for [Lce] type within a [ProducerScope].
* It provides methods to handle [Lce] instances and raise errors within a [Flow].
*
* @property raise The [LceRaise] instance that this class wraps.
* @property scope The [ProducerScope] that this class operates within.
* @property ifLoading The function to call if a loading state is raised.
*/
class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
private val raise: LceRaise<E>,
private val scope: ProducerScope<Lce<E, C>>,
private val ifLoading: LceRaise<E>.(C) -> Lce<E, C>,
) : Raise<Lce<E, Nothing>> by raise,
CoroutineScope by scope {
/**
* Raises an [Lce] instance within the [ProducerScope].
* It closes the [ProducerScope] after raise.
*
* @param r The [Lce] instance to raise.
*/
override fun raise(r: Lce<E, Nothing>): Nothing {
scope.launch(NonCancellable) {
scope.send(r)
scope.close()
}
raise.raise(r)
}
/**
* Sends a content value within the [ProducerScope].
* If the content is still loading, it calls [ifLoading] lambda to retrieve a state.
* Otherwise, it wraps the content in a [Lce.Content] state.
*
* @param content The content value to send.
* @param isStillLoading A flag indicating whether the content is still loading.
*/
suspend fun send(content: C, isStillLoading: Boolean = false) {
val value = if (isStillLoading) {
ifLoading(raise, content)
} else {
content.lceContent()
}
scope.send(value)
}
}
/**
* Creates a [LceFlow] by executing the given [block] within a [LceFlowScope] context.
*
* Flow starts with a [Lce.Loading] state.
*
* @param ifLoading The function to call if the [block] raises a [Lce.Loading] state.
* By default, it creates a new [Lce.Loading] state with the value returned by the [block].
* @param block The block to execute within a [LceFlowScope] context.
* @return A [LceFlow] representing the result of the [block].
*/
@OptIn(ExperimentalTypeInference::class)
fun <E : Any, C : Any> lceFlow(
ifLoading: LceRaise<E>.(C) -> Lce<E, C> = { lceLoading(partialContent = it) },
@BuilderInference block: suspend LceFlowScope<E, C>.() -> Unit,
): LceFlow<E, C> {
return channelFlow {
trySend(lceLoading())
lce {
val scope = LceFlowScope(
raise = this@lce,
scope = this@channelFlow,
ifLoading = ifLoading,
)
block(scope)
}
}
}

View file

@ -0,0 +1,86 @@
package com.tangem.domain.core.lce
import arrow.atomic.Atomic
import arrow.core.raise.Raise
import arrow.core.raise.recover
import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceLoading
import kotlin.experimental.ExperimentalTypeInference
/**
* A class that wraps a [Raise] instance for [Lce] type.
* It provides methods to handle [Lce] instances and raise errors.
*
* @property raise The [Raise] instance that this class wraps.
* @property isLoading An [Atomic] boolean flag indicating whether a loading operation is in progress.
*/
class LceRaise<E : Any> @PublishedApi internal constructor(
private val raise: Raise<Lce<E, Nothing>>,
) : Raise<Lce<E, Nothing>> by raise {
val isLoading: Atomic<Boolean> = Atomic(false)
/**
* 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.
*/
fun <C : Any> Lce<E, C>.bind(ifLoading: (partialContent: C?) -> C = { raise(lceLoading()) }): C = when (this) {
is Lce.Loading -> {
isLoading.set(true)
ifLoading(partialContent)
}
is Lce.Content -> content
is Lce.Error -> raise(r = this)
}
/**
* 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 returns the partial content.
* If this is a [Lce.Content] state, returns the content.
* If this is a [Lce.Error] state, raises the error.
*
* @return The content of this [Lce] instance.
*/
fun <C : Any> Lce<E, C>.bindOrNull(): C? = when (this) {
is Lce.Loading -> {
isLoading.set(true)
partialContent
}
is Lce.Content -> content
is Lce.Error -> raise(r = this)
}
}
/**
* Creates a [Lce] instance by executing the given [block] within a [LceRaise] context.
*
* @param ifLoading The function to call if the [block] raises a [Lce.Loading] state.
* By default, it creates a new [Lce.Loading] state with the value returned by the [block].
* @param block The block to execute within a [LceRaise] context.
* @return A [Lce] instance representing the result of the [block].
*/
@OptIn(ExperimentalTypeInference::class)
inline fun <E : Any, C : Any> lce(
ifLoading: LceRaise<E>.(C) -> Lce<E, C> = { lceLoading(partialContent = it) },
@BuilderInference block: LceRaise<E>.() -> C,
): Lce<E, C> = recover(
block = {
val raise = LceRaise(raise = this)
val value = block(raise)
if (raise.isLoading.get()) {
ifLoading(raise, value)
} else {
value.lceContent()
}
},
recover = { e: Lce<E, Nothing> -> e },
)

View file

@ -0,0 +1,37 @@
package com.tangem.domain.core.utils
import arrow.core.Either
import com.tangem.domain.core.lce.Lce
import kotlinx.coroutines.flow.Flow
/**
* [Flow] of [Either]
*
* @param E type of left value
* @param A type of right value
* */
typealias EitherFlow<E, A> = Flow<Either<E, A>>
/**
* Converts an [Either] instance to a [Lce] instance.
* If this is a [Either.Left], it is converted to a [Lce.Error] with the same error.
* If this is a [Either.Right], it is converted to a [Lce.Content] or [Lce.Loading] with the same content,
* depending on the [isStillLoading] parameter.
*
* @param isStillLoading A flag indicating whether the content is still loading.
* If true, the [Either.Right] is converted to a [Lce.Loading].
* @return A [Lce] instance containing the same content or error as this [Either],
* and possibly indicating a loading state.
*/
inline fun <reified E : Any, reified T : Any> Either<E, T>.toLce(isStillLoading: Boolean = false): Lce<E, T> {
return when (this) {
is Either.Left -> Lce.Error(value)
is Either.Right -> {
if (isStillLoading) {
Lce.Loading(value)
} else {
Lce.Content(value)
}
}
}
}

View file

@ -0,0 +1,75 @@
package com.tangem.domain.core.utils
import arrow.core.Either
import arrow.core.identity
import arrow.core.left
import arrow.core.right
import com.tangem.domain.core.lce.Lce
/**
* Creates a [Lce.Loading] instance with optional partial content.
*
* @param partialContent The partial content that has been loaded so far, if any.
* @return A [Lce.Loading] instance.
*/
fun <C : Any> lceLoading(partialContent: C? = null): Lce<Nothing, C> = Lce.Loading(partialContent)
/**
* Wraps the receiver object in a [Lce.Content] instance.
*
* @return A [Lce.Content] instance containing the receiver object.
*/
fun <C : Any> C.lceContent(): Lce<Nothing, C> = Lce.Content(content = this)
/**
* Wraps the receiver object in a [Lce.Error] instance.
*
* @return A [Lce.Error] instance containing the receiver object.
*/
fun <E : Any> E.lceError(): Lce<E, Nothing> = Lce.Error(error = this)
/**
* Transforms this [Lce] instance by applying the given function into a new [Lce] instance.
*
* @param block The function to apply to the content of this [Lce].
* @return A new [Lce] instance containing the result of applying the function.
* */
inline fun <E : Any, C : Any, T : Any> Lce<E, C>.flatMap(
block: (content: C, isLoading: Boolean) -> Lce<E, T>,
): Lce<E, T> = when (this) {
is Lce.Loading -> partialContent?.let { block(it, true) } ?: lceLoading()
is Lce.Content -> block(content, false)
is Lce.Error -> this
}
/**
* Returns the content of this [Lce] if it's a [Lce.Content] or applies the given functions if it's a [Lce.Loading] or [Lce.Error].
*
* @param ifLoading The function to apply if this is a [Lce.Loading] state.
* @param ifError The function to apply if this is a [Lce.Error] state.
* @return The content of this [Lce] or the result of applying the corresponding function.
*/
inline fun <E : Any, C : Any> Lce<E, C>.getOrElse(ifLoading: (maybeContent: C?) -> C, ifError: (error: E) -> C): C {
return fold(
ifLoading = ifLoading,
ifContent = ::identity,
ifError = ifError,
)
}
/**
* Transforms this [Lce] into an [Either] instance.
* If this is a [Lce.Content], the content is wrapped in a [Either.Right].
* If this is a [Lce.Error], the error is wrapped in a [Either.Left].
* If this is a [Lce.Loading], the [ifLoading] function is applied to the partial content and the result is wrapped in a
* [Either.Right].
*
* @param ifLoading The function to apply if this is a [Lce.Loading] state.
* @return An [Either] instance containing the content or error of this [Lce],
* or the result of applying the [ifLoading] function to the partial content.
*/
inline fun <E : Any, C : Any> Lce<E, C>.toEither(ifLoading: (maybeContent: C?) -> C): Either<E, C> = fold(
ifLoading = { ifLoading(it).right() },
ifContent = { it.right() },
ifError = { it.left() },
)

View file

@ -13,4 +13,5 @@ dependencies {
implementation(deps.jodatime)
implementation(projects.core.res)
implementation(projects.domain.wallets.models)
}

View file

@ -1,9 +1,6 @@
package com.tangem.domain.feedback
import com.tangem.domain.feedback.models.BlockchainInfo
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.feedback.models.PhoneInfo
import com.tangem.domain.feedback.models.UserWalletsInfo
import com.tangem.domain.feedback.models.*
import com.tangem.domain.feedback.utils.breakLine
import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainAddresses
@ -68,6 +65,24 @@ internal class FeedbackDataBuilder {
builder.appendKeyValue("App version", phoneInfo.appVersion)
}
fun addBlockchainError(info: BlockchainInfo, error: BlockchainErrorInfo) {
builder.appendKeyValue("Blockchain", info.blockchain)
builder.appendKeyValue("Derivation path", info.derivationPath)
builder.appendKeyValue("Host", info.host)
builder.appendKeyValue("Token", error.tokenSymbol)
builder.appendKeyValue("Error", error.errorMessage)
builder.appendDelimiter()
builder.appendAddresses(
key = "Source address${info.addresses.isMultiple(suffix = "es")}",
addresses = info.addresses,
)
builder.appendKeyValue("Destination address", error.destinationAddress)
builder.appendKeyValue("Amount", error.amount)
builder.appendKeyValue("Fee", error.fee ?: "Unable to receive")
}
fun addDelimiter(): StringBuilder = builder.appendDelimiter()
fun build(): String = builder.trimEnd().toString()

View file

@ -0,0 +1,67 @@
package com.tangem.domain.feedback
import android.content.res.Resources
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.feedback.models.FeedbackEmail
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.feedback.utils.*
/**
* Get email with feedback for support
*
* @property feedbackRepository feedback repository
* @property resources resources for getting strings
*
[REDACTED_AUTHOR]
*/
class GetFeedbackEmailUseCase(
private val feedbackRepository: FeedbackRepository,
private val resources: Resources,
) {
private val emailSubjectResolver = EmailSubjectResolver(resources)
private val emailMessageTitleResolver = EmailMessageTitleResolver(resources)
private val emailMessageBodyResolver = EmailMessageBodyResolver(feedbackRepository)
suspend operator fun invoke(type: FeedbackEmailType): FeedbackEmail {
val cardInfo = feedbackRepository.getCardInfo()
val formattedLogs = AppLogsFormatter().format(appLogs = feedbackRepository.getAppLogs())
return FeedbackEmail(
address = getAddress(cardInfo),
subject = emailSubjectResolver.resolve(type, cardInfo),
message = createMessage(type, cardInfo),
file = feedbackRepository.createLogFile(logs = formattedLogs),
)
}
private fun getAddress(cardInfo: CardInfo): String {
return if (cardInfo.isStart2Coin) START2COIN_SUPPORT_EMAIL else TANGEM_SUPPORT_EMAIL
}
private suspend fun createMessage(type: FeedbackEmailType, cardInfo: CardInfo): String {
return StringBuilder().apply {
val title = emailMessageTitleResolver.resolve(type)
append(title)
skipLine()
appendDisclaimerIfNeeded(type)
skipLine()
val body = emailMessageBodyResolver.resolve(type, cardInfo)
append(body)
}.toString()
}
private fun StringBuilder.appendDisclaimerIfNeeded(type: FeedbackEmailType): StringBuilder {
return if (type is FeedbackEmailType.ScanningProblem) {
this
} else {
append(resources.getString(R.string.feedback_data_collection_message))
}
}
}

View file

@ -1,113 +0,0 @@
package com.tangem.domain.feedback
import android.content.res.Resources
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.feedback.models.SupportFeedbackEmail
import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.feedback.utils.skipLine
import org.joda.time.DateTime
import org.joda.time.format.DateTimeFormatterBuilder
import java.util.Locale
/**
* Get email with feedback for support
*
* @property feedbackRepository feedback repository
* @property resources resources for getting strings
*
[REDACTED_AUTHOR]
*/
class GetSupportFeedbackEmailUseCase(
private val feedbackRepository: FeedbackRepository,
private val resources: Resources,
) {
// 00.00 00:00:00.000
private val dateFormatter = DateTimeFormatterBuilder()
.appendDayOfMonth(2)
.appendLiteral('.')
.appendMonthOfYear(2)
.appendLiteral(' ')
.appendHourOfDay(2)
.appendLiteral(':')
.appendMinuteOfHour(2)
.appendLiteral(':')
.appendSecondOfMinute(2)
.appendLiteral('.')
.appendMillisOfSecond(3)
.toFormatter()
.withLocale(Locale.getDefault())
suspend operator fun invoke(): SupportFeedbackEmail {
val cardInfo = feedbackRepository.getCardInfo()
return SupportFeedbackEmail(
address = getEmail(isStart2Coin = cardInfo.isStart2Coin),
subject = getSubject(isStart2Coin = cardInfo.isStart2Coin),
message = createMessage(cardInfo),
file = feedbackRepository.createLogFile(logs = getLogs()),
)
}
private fun getEmail(isStart2Coin: Boolean): String {
return if (isStart2Coin) START2COIN_SUPPORT_EMAIL else TANGEM_SUPPORT_EMAIL
}
private fun getSubject(isStart2Coin: Boolean): String {
return resources.getString(
if (isStart2Coin) {
R.string.feedback_subject_support
} else {
R.string.feedback_subject_support_tangem
},
)
}
private suspend fun createMessage(cardInfo: CardInfo): String {
return StringBuilder().apply {
append(resources.getString(R.string.feedback_preface_support))
skipLine()
append(resources.getString(R.string.feedback_data_collection_message))
skipLine()
append(
FeedbackDataBuilder().apply {
addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo())
addDelimiter()
addCardInfo(cardInfo)
addDelimiter()
addBlockchainInfoList(blockchainInfoList = feedbackRepository.getBlockchainInfoList())
addDelimiter()
addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())
}.build(),
)
}.toString()
}
private suspend fun getLogs(): String {
val builder = StringBuilder()
var sum = 0
val appLogs = feedbackRepository.getAppLogs()
for (i in appLogs.lastIndex downTo 0) {
val log = appLogs[i]
val date = dateFormatter.print(DateTime(log.timestamp))
val formattedLog = "$date: ${log.message}\n"
sum += formattedLog.length
if (sum < GMAIL_MAX_FILE_SIZE) {
builder.insert(0, formattedLog)
} else {
break
}
}
return builder.toString()
}
private companion object {
const val START2COIN_SUPPORT_EMAIL = "cardsupport@start2coin.com"
const val TANGEM_SUPPORT_EMAIL = "support@tangem.com"
const val GMAIL_MAX_FILE_SIZE = 24_900_000 // ≈ 25 MB
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.domain.feedback
import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.domain.feedback.repository.FeedbackRepository
/**
* Save last blockchain error
*
* @property feedbackRepository feedback repository
*
[REDACTED_AUTHOR]
*/
class SaveBlockchainErrorUseCase(
private val feedbackRepository: FeedbackRepository,
) {
fun invoke(error: BlockchainErrorInfo) {
feedbackRepository.saveBlockchainErrorInfo(error = error)
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.domain.feedback.models
/**
* Information about blockchain's operation error
*
* @property errorMessage message about error
* @property blockchainId blockchain id
* @property derivationPath derivation path
* @property destinationAddress destination address
* @property tokenSymbol token symbol or null, if it isn't operation with token
* @property amount amount
* @property fee fee or null, if unable to get
*/
data class BlockchainErrorInfo(
val errorMessage: String,
val blockchainId: String,
val derivationPath: String?,
val destinationAddress: String,
val tokenSymbol: String?,
val amount: String,
val fee: String?,
)

View file

@ -2,7 +2,7 @@ package com.tangem.domain.feedback.models
import java.io.File
data class SupportFeedbackEmail(
data class FeedbackEmail(
val address: String,
val subject: String,
val message: String,

View file

@ -0,0 +1,21 @@
package com.tangem.domain.feedback.models
/**
* Email feedback type
*
[REDACTED_AUTHOR]
*/
sealed interface FeedbackEmailType {
/** User initiate request yourself. Example, button on DetailsScreen or OnboardingScreen */
data object DirectUserRequest : FeedbackEmailType
/** User rate the app as "can be better" */
data object RateCanBeBetter : FeedbackEmailType
/** User has problem with scanning */
data object ScanningProblem : FeedbackEmailType
/** User has problem with sending transaction */
data object TransactionSendingProblem : FeedbackEmailType
}

View file

@ -11,8 +11,14 @@ interface FeedbackRepository {
suspend fun getBlockchainInfoList(): List<BlockchainInfo>
suspend fun getBlockchainInfo(blockchainId: String, derivationPath: String?): BlockchainInfo?
fun getPhoneInfo(): PhoneInfo
fun saveBlockchainErrorInfo(error: BlockchainErrorInfo)
suspend fun getBlockchainErrorInfo(): BlockchainErrorInfo?
suspend fun getAppLogs(): List<AppLogModel>
suspend fun createLogFile(logs: String): File?

View file

@ -0,0 +1,61 @@
package com.tangem.domain.feedback.utils
import com.tangem.domain.feedback.models.AppLogModel
import org.joda.time.DateTime
import org.joda.time.format.DateTimeFormatter
import org.joda.time.format.DateTimeFormatterBuilder
import java.util.Locale
/**
* App logs formatter
*
[REDACTED_AUTHOR]
*/
internal class AppLogsFormatter {
private val dateFormatter = createDateFormatter()
/** Format [appLogs] to [String] */
fun format(appLogs: List<AppLogModel>): String {
val builder = StringBuilder()
var sum = 0
for (i in appLogs.lastIndex downTo 0) {
val log = appLogs[i]
val date = dateFormatter.print(DateTime(log.timestamp))
val formattedLog = "$date: ${log.message}\n"
sum += formattedLog.length
if (sum < GMAIL_MAX_FILE_SIZE) {
builder.insert(0, formattedLog)
} else {
break
}
}
return builder.toString()
}
// Example, 00.00 00:00:00.000
private fun createDateFormatter(): DateTimeFormatter {
return DateTimeFormatterBuilder()
.appendDayOfMonth(2)
.appendLiteral('.')
.appendMonthOfYear(2)
.appendLiteral(' ')
.appendHourOfDay(2)
.appendLiteral(':')
.appendMinuteOfHour(2)
.appendLiteral(':')
.appendSecondOfMinute(2)
.appendLiteral('.')
.appendMillisOfSecond(MIN_MILLIS_DIGITS)
.toFormatter()
.withLocale(Locale.getDefault())
}
private companion object {
const val MIN_MILLIS_DIGITS = 3
}
}

View file

@ -0,0 +1,70 @@
package com.tangem.domain.feedback.utils
import com.tangem.domain.feedback.FeedbackDataBuilder
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.feedback.repository.FeedbackRepository
/**
* Email message body resolver
*
* @property feedbackRepository feedback repository
*
[REDACTED_AUTHOR]
*/
internal class EmailMessageBodyResolver(
private val feedbackRepository: FeedbackRepository,
) {
/** Resolve email message body by [type] using [cardInfo] */
suspend fun resolve(type: FeedbackEmailType, cardInfo: CardInfo): String = with(FeedbackDataBuilder()) {
when (type) {
FeedbackEmailType.DirectUserRequest -> addUserRequestBody(cardInfo)
FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(cardInfo)
FeedbackEmailType.ScanningProblem -> addScanningProblemBody()
FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(cardInfo)
}
return build()
}
private suspend fun FeedbackDataBuilder.addUserRequestBody(cardInfo: CardInfo) {
addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo())
addDelimiter()
addCardInfo(cardInfo)
addDelimiter()
addBlockchainInfoList(blockchainInfoList = feedbackRepository.getBlockchainInfoList())
addDelimiter()
addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())
}
private fun FeedbackDataBuilder.addScanningProblemBody() {
addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())
}
private suspend fun FeedbackDataBuilder.addTransactionSendingProblemBody(cardInfo: CardInfo) {
addCardInfo(cardInfo)
addDelimiter()
val blockchainError = feedbackRepository.getBlockchainErrorInfo()
val blockchainInfo = blockchainError?.let {
feedbackRepository.getBlockchainInfo(
blockchainId = blockchainError.blockchainId,
derivationPath = blockchainError.derivationPath,
)
}
if (blockchainInfo != null) {
addBlockchainError(blockchainInfo, blockchainError)
addDelimiter()
}
addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())
}
private fun FeedbackDataBuilder.addCardAndPhoneInfo(cardInfo: CardInfo) {
addCardInfo(cardInfo)
addDelimiter()
addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.domain.feedback.utils
import android.content.res.Resources
import com.tangem.domain.feedback.R
import com.tangem.domain.feedback.models.FeedbackEmailType
/**
* Email message title resolver
*
* @property resources resources
*
[REDACTED_AUTHOR]
*/
internal class EmailMessageTitleResolver(private val resources: Resources) {
/** Resolve email message title by [type] */
fun resolve(type: FeedbackEmailType): String {
return when (type) {
FeedbackEmailType.DirectUserRequest -> R.string.feedback_preface_support
FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative
FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed
FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_preface_tx_failed
}
.let(resources::getString)
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.domain.feedback.utils
import android.content.res.Resources
import com.tangem.domain.feedback.R
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.feedback.models.FeedbackEmailType
/**
* Email subject resolver
*
* @property resources resources
*
[REDACTED_AUTHOR]
*/
internal class EmailSubjectResolver(private val resources: Resources) {
/** Resolve email message body by [type] using [cardInfo] */
fun resolve(type: FeedbackEmailType, cardInfo: CardInfo): String {
return when (type) {
FeedbackEmailType.DirectUserRequest -> {
if (cardInfo.isStart2Coin) {
R.string.feedback_subject_support
} else {
R.string.feedback_subject_support_tangem
}
}
FeedbackEmailType.RateCanBeBetter -> R.string.feedback_subject_rate_negative
FeedbackEmailType.ScanningProblem -> R.string.feedback_subject_scan_failed
FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_subject_tx_failed
}
.let(resources::getString)
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.domain.feedback.utils
internal const val GMAIL_MAX_FILE_SIZE = 24_900_000 // ≈ 25 MB
internal const val START2COIN_SUPPORT_EMAIL = "cardsupport@start2coin.com"
internal const val TANGEM_SUPPORT_EMAIL = "support@tangem.com"

View file

@ -9,16 +9,17 @@ android {
}
dependencies {
implementation(project(":core:datasource"))
implementation(project(":core:utils"))
implementation(project(":common"))
implementation(project(":libs:auth"))
implementation(projects.core.datasource)
implementation(projects.core.utils)
implementation(projects.common)
implementation(projects.libs.auth)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.demo)
implementation(projects.domain.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.transaction.models)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.wallets.models)
/** Tangem libraries */
implementation(deps.tangem.blockchain) {
exclude(module = "joda-time")

View file

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

View file

@ -1,338 +0,0 @@
package com.tangem.domain.common.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import java.math.BigDecimal
@Suppress("ComplexMethod", "LongMethod")
fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
return when (networkId) {
"arbitrum-one" -> Blockchain.Arbitrum
"arbitrum-one/test" -> Blockchain.ArbitrumTestnet
"avalanche", "avalanche-2" -> Blockchain.Avalanche
"avalanche/test", "avalanche-2/test" -> Blockchain.AvalancheTestnet
"binancecoin" -> Blockchain.Binance
"binancecoin/test" -> Blockchain.BinanceTestnet
"binance-smart-chain" -> Blockchain.BSC
"binance-smart-chain/test" -> Blockchain.BSCTestnet
"ethereum" -> Blockchain.Ethereum
"ethereum/test" -> Blockchain.EthereumTestnet
"ethereum-classic" -> Blockchain.EthereumClassic
"ethereum-classic/test" -> Blockchain.EthereumClassicTestnet
"polygon-pos", "matic-network" -> Blockchain.Polygon
"polygon-pos/test", "matic-network/test" -> Blockchain.PolygonTestnet
"solana" -> Blockchain.Solana
"solana/test" -> Blockchain.SolanaTestnet
"fantom" -> Blockchain.Fantom
"fantom/test" -> Blockchain.FantomTestnet
"bitcoin" -> Blockchain.Bitcoin
"bitcoin/test" -> Blockchain.BitcoinTestnet
"bitcoin-cash" -> Blockchain.BitcoinCash
"bitcoin-cash/test" -> Blockchain.BitcoinCashTestnet
"cardano" -> Blockchain.Cardano
"dogecoin" -> Blockchain.Dogecoin
"ducatus" -> Blockchain.Ducatus
"litecoin" -> Blockchain.Litecoin
"rootstock" -> Blockchain.RSK
"stellar" -> Blockchain.Stellar
"stellar/test" -> Blockchain.StellarTestnet
"tezos" -> Blockchain.Tezos
"tron" -> Blockchain.Tron
"tron/test" -> Blockchain.TronTestnet
"xrp", "ripple" -> Blockchain.XRP
"xdai" -> Blockchain.Gnosis
"ethereum-pow-iou" -> Blockchain.EthereumPow
"ethereum-pow-iou/test" -> Blockchain.EthereumPowTestnet
"ethereumfair", "dischain" -> Blockchain.Dischain // for old client compatibility
"polkadot" -> Blockchain.Polkadot
"polkadot/test" -> Blockchain.PolkadotTestnet
"kusama" -> Blockchain.Kusama
"optimistic-ethereum" -> Blockchain.Optimism
"optimistic-ethereum/test" -> Blockchain.OptimismTestnet
"dash" -> Blockchain.Dash
"kaspa" -> Blockchain.Kaspa
"the-open-network" -> Blockchain.TON
"the-open-network/test" -> Blockchain.TONTestnet
"kava" -> Blockchain.Kava
"kava/test" -> Blockchain.KavaTestnet
"ravencoin" -> Blockchain.Ravencoin
"ravencoin/test" -> Blockchain.RavencoinTestnet
"cosmos" -> Blockchain.Cosmos
"cosmos/test" -> Blockchain.CosmosTestnet
"terra" -> Blockchain.TerraV1
"terra-2" -> Blockchain.TerraV2
"cronos" -> Blockchain.Cronos
"telos" -> Blockchain.Telos
"telos/test" -> Blockchain.TelosTestnet
"aleph-zero" -> Blockchain.AlephZero
"aleph-zero/test" -> Blockchain.AlephZeroTestnet
"octaspace" -> Blockchain.OctaSpace
"octaspace/test" -> Blockchain.OctaSpaceTestnet
"chia" -> Blockchain.Chia
"chia/test" -> Blockchain.ChiaTestnet
"near-protocol" -> Blockchain.Near
"near-protocol/test" -> Blockchain.NearTestnet
"decimal" -> Blockchain.Decimal
"decimal/test" -> Blockchain.DecimalTestnet
"xdc-network" -> Blockchain.XDC
"xdc-network/test" -> Blockchain.XDCTestnet
"vechain" -> Blockchain.VeChain
"vechain/test" -> Blockchain.VeChainTestnet
"aptos" -> Blockchain.Aptos
"aptos/test" -> Blockchain.AptosTestnet
"playa3ull-games" -> Blockchain.Playa3ull
"shibarium" -> Blockchain.Shibarium
"shibarium/test" -> Blockchain.ShibariumTestnet
"algorand" -> Blockchain.Algorand
"algorand/test" -> Blockchain.AlgorandTestnet
"hedera-hashgraph" -> Blockchain.Hedera
"hedera-hashgraph/test" -> Blockchain.HederaTestnet
"aurora" -> Blockchain.Aurora
"aurora/test" -> Blockchain.AuroraTestnet
"areon-network" -> Blockchain.Areon
"areon-network/test" -> Blockchain.AreonTestnet
"pulsechain" -> Blockchain.PulseChain
"pulsechain/test" -> Blockchain.PulseChainTestnet
"zksync" -> Blockchain.ZkSyncEra
"zksync/test" -> Blockchain.ZkSyncEraTestnet
"moonbeam" -> Blockchain.Moonbeam
"moonbeam/test" -> Blockchain.MoonbeamTestnet
"manta-network" -> Blockchain.Manta
"manta-network/test" -> Blockchain.MantaTestnet
"polygon-zkevm" -> Blockchain.PolygonZkEVM
"polygon-zkevm/test" -> Blockchain.PolygonZkEVMTestnet
"nexa" -> Blockchain.Nexa // FIXME
"nexa/test" -> Blockchain.NexaTestnet // FIXME
"radiant" -> Blockchain.Radiant
"moonriver" -> Blockchain.Moonriver
"moonriver/test" -> Blockchain.MoonriverTestnet
"mantle" -> Blockchain.Mantle
"mantle/test" -> Blockchain.MantleTestnet
"flare-network" -> Blockchain.Flare
"flare-network/test" -> Blockchain.FlareTestnet
"taraxa" -> Blockchain.Taraxa
"taraxa/test" -> Blockchain.TaraxaTestnet
"base" -> Blockchain.Base
"base/test" -> Blockchain.BaseTestnet
else -> null
}
}
@Suppress("ComplexMethod", "LongMethod")
fun Blockchain.toNetworkId(): String {
return when (this) {
Blockchain.Unknown -> "unknown"
Blockchain.Arbitrum -> "arbitrum-one"
Blockchain.ArbitrumTestnet -> "arbitrum-one/test"
Blockchain.Avalanche -> "avalanche"
Blockchain.AvalancheTestnet -> "avalanche/test"
Blockchain.Binance -> "binancecoin"
Blockchain.BinanceTestnet -> "binancecoin/test"
Blockchain.BSC -> "binance-smart-chain"
Blockchain.BSCTestnet -> "binance-smart-chain/test"
Blockchain.Bitcoin -> "bitcoin"
Blockchain.BitcoinTestnet -> "bitcoin/test"
Blockchain.BitcoinCash -> "bitcoin-cash"
Blockchain.BitcoinCashTestnet -> "bitcoin-cash/test"
Blockchain.Cardano -> "cardano"
Blockchain.Dogecoin -> "dogecoin"
Blockchain.Ducatus -> "ducatus"
Blockchain.Ethereum -> "ethereum"
Blockchain.EthereumTestnet -> "ethereum/test"
Blockchain.EthereumClassic -> "ethereum-classic"
Blockchain.EthereumClassicTestnet -> "ethereum-classic/test"
Blockchain.Fantom -> "fantom"
Blockchain.FantomTestnet -> "fantom/test"
Blockchain.Litecoin -> "litecoin"
Blockchain.Polygon -> "polygon-pos"
Blockchain.PolygonTestnet -> "polygon-pos/test"
Blockchain.RSK -> "rootstock"
Blockchain.Stellar -> "stellar"
Blockchain.StellarTestnet -> "stellar/test"
Blockchain.Solana -> "solana"
Blockchain.SolanaTestnet -> "solana/test"
Blockchain.Tezos -> "tezos"
Blockchain.XRP -> "xrp"
Blockchain.Tron -> "tron"
Blockchain.TronTestnet -> "tron/test"
Blockchain.Gnosis -> "xdai"
Blockchain.EthereumPow -> "ethereum-pow-iou"
Blockchain.EthereumPowTestnet -> "ethereum-pow-iou/test"
Blockchain.Dischain -> "ethereumfair" // for backend compatibility
Blockchain.Polkadot -> "polkadot"
Blockchain.PolkadotTestnet -> "polkadot/test"
Blockchain.Kusama -> "kusama"
Blockchain.Optimism -> "optimistic-ethereum"
Blockchain.OptimismTestnet -> "optimistic-ethereum/test"
Blockchain.Dash -> "dash"
Blockchain.Kaspa -> "kaspa"
Blockchain.TON -> "the-open-network"
Blockchain.TONTestnet -> "the-open-network/test"
Blockchain.Kava -> "kava"
Blockchain.KavaTestnet -> "kava/test"
Blockchain.Ravencoin -> "ravencoin"
Blockchain.RavencoinTestnet -> "ravencoin/test"
Blockchain.Cosmos -> "cosmos"
Blockchain.CosmosTestnet -> "cosmos/test"
Blockchain.TerraV1 -> "terra"
Blockchain.TerraV2 -> "terra-2"
Blockchain.Cronos -> "cronos"
Blockchain.Telos -> "telos"
Blockchain.TelosTestnet -> "telos/test"
Blockchain.AlephZero -> "aleph-zero"
Blockchain.AlephZeroTestnet -> "aleph-zero/test"
Blockchain.OctaSpace -> "octaspace"
Blockchain.OctaSpaceTestnet -> "octaspace/test"
Blockchain.Chia -> "chia"
Blockchain.ChiaTestnet -> "chia/test"
Blockchain.Near -> "near-protocol"
Blockchain.NearTestnet -> "near-protocol/test"
Blockchain.Decimal -> "decimal"
Blockchain.DecimalTestnet -> "decimal/test"
Blockchain.XDC -> "xdc-network"
Blockchain.XDCTestnet -> "xdc-network/test"
Blockchain.VeChain -> "vechain"
Blockchain.VeChainTestnet -> "vechain/test"
Blockchain.Aptos -> "aptos"
Blockchain.AptosTestnet -> "aptos/test"
Blockchain.Playa3ull -> "playa3ull-games"
Blockchain.Shibarium -> "shibarium"
Blockchain.ShibariumTestnet -> "shibarium/test"
Blockchain.Algorand -> "algorand"
Blockchain.AlgorandTestnet -> "algorand/test"
Blockchain.Hedera -> "hedera-hashgraph"
Blockchain.HederaTestnet -> "hedera-hashgraph/test"
Blockchain.Aurora -> "aurora"
Blockchain.AuroraTestnet -> "aurora/test"
Blockchain.Areon -> "areon-network"
Blockchain.AreonTestnet -> "areon-network/test"
Blockchain.PulseChain -> "pulsechain"
Blockchain.PulseChainTestnet -> "pulsechain/test"
Blockchain.ZkSyncEra -> "zksync"
Blockchain.ZkSyncEraTestnet -> "zksync/test"
Blockchain.Moonbeam -> "moonbeam"
Blockchain.MoonbeamTestnet -> "moonbeam/test"
Blockchain.Manta -> "manta-network"
Blockchain.MantaTestnet -> "manta-network/test"
Blockchain.PolygonZkEVM -> "polygon-zkevm"
Blockchain.PolygonZkEVMTestnet -> "polygon-zkevm/test"
Blockchain.Nexa -> "nexa" // FIXME
Blockchain.NexaTestnet -> "nexa/test" // FIXME
Blockchain.Radiant -> "radiant"
Blockchain.Moonriver -> "moonriver"
Blockchain.MoonriverTestnet -> "moonriver/test"
Blockchain.Mantle -> "mantle"
Blockchain.MantleTestnet -> "mantle/test"
Blockchain.Flare -> "flare-network"
Blockchain.FlareTestnet -> "flare-network/test"
Blockchain.Taraxa -> "taraxa"
Blockchain.TaraxaTestnet -> "taraxa/test"
Blockchain.Base -> "base"
Blockchain.BaseTestnet -> "base/test"
}
}
@Suppress("ComplexMethod", "LongMethod")
fun Blockchain.toCoinId(): String {
return when (this) {
Blockchain.Binance, Blockchain.BinanceTestnet, Blockchain.BSC, Blockchain.BSCTestnet -> "binancecoin"
Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> "bitcoin"
Blockchain.BitcoinCash, Blockchain.BitcoinCashTestnet -> "bitcoin-cash"
Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ethereum"
Blockchain.EthereumClassic, Blockchain.EthereumClassicTestnet -> "ethereum-classic"
Blockchain.Stellar, Blockchain.StellarTestnet -> "stellar"
Blockchain.Cardano -> "cardano"
Blockchain.Polygon, Blockchain.PolygonTestnet -> "matic-network"
Blockchain.Arbitrum, Blockchain.ArbitrumTestnet -> "arbitrum-one"
Blockchain.Avalanche, Blockchain.AvalancheTestnet -> "avalanche-2"
Blockchain.Solana, Blockchain.SolanaTestnet -> "solana"
Blockchain.Fantom, Blockchain.FantomTestnet -> "fantom"
Blockchain.Tron, Blockchain.TronTestnet -> "tron"
Blockchain.Polkadot, Blockchain.PolkadotTestnet -> "polkadot"
Blockchain.Ducatus -> "ducatus"
Blockchain.Litecoin -> "litecoin"
Blockchain.RSK -> "rootstock"
Blockchain.Tezos -> "tezos"
Blockchain.XRP -> "ripple"
Blockchain.Dogecoin -> "dogecoin"
Blockchain.Gnosis -> "xdai"
Blockchain.EthereumPow, Blockchain.EthereumPowTestnet -> "ethereum-pow-iou"
Blockchain.Dischain -> "ethereumfair" // for backend compatibility
Blockchain.Kusama -> "kusama"
Blockchain.Optimism, Blockchain.OptimismTestnet -> "optimistic-ethereum"
Blockchain.Dash -> "dash"
Blockchain.Kaspa -> "kaspa"
Blockchain.TON, Blockchain.TONTestnet -> "the-open-network"
Blockchain.Kava, Blockchain.KavaTestnet -> "kava"
Blockchain.Ravencoin, Blockchain.RavencoinTestnet -> "ravencoin"
Blockchain.Cosmos, Blockchain.CosmosTestnet -> "cosmos"
Blockchain.TerraV1 -> "terra-luna"
Blockchain.TerraV2 -> "terra-luna-2"
Blockchain.Cronos -> "crypto-com-chain"
Blockchain.Telos, Blockchain.TelosTestnet -> "telos"
Blockchain.AlephZero, Blockchain.AlephZeroTestnet -> "aleph-zero"
Blockchain.OctaSpace, Blockchain.OctaSpaceTestnet -> "octaspace"
Blockchain.Chia, Blockchain.ChiaTestnet -> "chia"
Blockchain.Near -> "near"
Blockchain.NearTestnet -> "near/test"
Blockchain.Decimal, Blockchain.DecimalTestnet -> "decimal"
Blockchain.XDC, Blockchain.XDCTestnet -> "xdce-crowd-sale"
Blockchain.VeChain, Blockchain.VeChainTestnet -> "vechain"
Blockchain.Aptos -> "aptos"
Blockchain.AptosTestnet -> "aptos/test"
Blockchain.Playa3ull -> "playa3ull-games-2"
Blockchain.Shibarium -> "bone-shibaswap"
Blockchain.ShibariumTestnet -> "bone-shibaswap/test"
Blockchain.Algorand -> "algorand"
Blockchain.AlgorandTestnet -> "algorand/test"
Blockchain.Unknown -> "unknown"
Blockchain.Hedera -> "hedera-hashgraph"
Blockchain.HederaTestnet -> "hedera-hashgraph/test"
Blockchain.Aurora, Blockchain.AuroraTestnet -> "aurora-ethereum"
Blockchain.Areon, Blockchain.AreonTestnet -> "areon-network"
Blockchain.PulseChain, Blockchain.PulseChainTestnet -> "pulsechain"
Blockchain.ZkSyncEra, Blockchain.ZkSyncEraTestnet -> "zksync-ethereum"
Blockchain.Moonbeam, Blockchain.MoonbeamTestnet -> "moonbeam"
Blockchain.Manta, Blockchain.MantaTestnet -> "manta-network-ethereum"
Blockchain.PolygonZkEVM, Blockchain.PolygonZkEVMTestnet -> "polygon-zkevm-ethereum"
Blockchain.Nexa, Blockchain.NexaTestnet -> "nexa" // FIXME
Blockchain.Radiant -> "radiant"
Blockchain.Moonriver, Blockchain.MoonriverTestnet -> "moonriver"
Blockchain.Mantle, Blockchain.MantleTestnet -> "mantle"
Blockchain.Flare, Blockchain.FlareTestnet -> "flare-networks"
Blockchain.Taraxa, Blockchain.TaraxaTestnet -> "taraxa"
Blockchain.Base, Blockchain.BaseTestnet -> "base-ethereum"
}
}
fun Blockchain.isSupportedInApp(): Boolean {
return !excludedBlockchains.contains(this)
}
fun Blockchain.amountToCreateAccount(token: Token? = null): BigDecimal? {
return when (this) {
Blockchain.Stellar -> if (token?.symbol == NODL) BigDecimal(NODL_AMOUNT_TO_CREATE_ACCOUNT) else BigDecimal.ONE
Blockchain.XRP -> BigDecimal.TEN
Blockchain.Near, Blockchain.NearTestnet -> 0.00182.toBigDecimal()
Blockchain.Aptos, Blockchain.AptosTestnet -> BigDecimal.ZERO
else -> null
}
}
fun Blockchain.minimalAmount(): BigDecimal {
return BigDecimal.ONE.movePointLeft(decimals())
}
private const val NODL = "NODL"
private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5
private val excludedBlockchains = listOf(
Blockchain.Unknown,
Blockchain.Nexa,
Blockchain.NexaTestnet,
Blockchain.Radiant,
Blockchain.Manta,
Blockchain.MantaTestnet,
Blockchain.Mantle,
Blockchain.MantleTestnet,
)

View file

@ -1,6 +1,7 @@
package com.tangem.domain.common.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.isSupportedInApp
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.common.CardTypesResolver
@ -22,7 +23,7 @@ fun CardDTO.supportedBlockchains(cardTypesResolver: CardTypesResolver): List<Blo
wallets.flatMap { Blockchain.fromCurve(it.curve) }.distinct().toMutableList()
} else {
// multiwallet supports all blockchains, move this logic to config
Blockchain.values().toMutableList()
Blockchain.entries.toMutableList()
}
return supportedBlockchains
.filter { isTestCard == it.isTestnet() }

View file

@ -6,6 +6,8 @@ import com.tangem.domain.tokens.model.CryptoCurrency
* Manager that holds info about available actions as Sell and Buy
*/
interface RampStateManager {
fun availableForBuy(cryptoCurrency: CryptoCurrency): Boolean
fun availableForSell(cryptoCurrency: CryptoCurrency): Boolean
}

View file

@ -2,11 +2,11 @@ package com.tangem.domain.redux.global
import android.webkit.ValueCallback
import com.tangem.common.extensions.toHexString
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.utils.RequestHeader
import com.tangem.domain.redux.BaseStoreHub
import com.tangem.domain.redux.DomainState
import com.tangem.domain.redux.ReStoreReducer
import com.tangem.lib.auth.AuthProvider
import org.rekotlin.Action
/**

View file

@ -1,12 +0,0 @@
package com.tangem.domain.walletconnect
import com.tangem.domain.wallets.models.UserWallet
import org.rekotlin.Action
sealed class WalletConnectActions : Action {
sealed class New {
data class Initialize(val userWallet: UserWallet) : WalletConnectActions()
data class SetupUserChains(val userWallet: UserWallet) : WalletConnectActions()
}
}

View file

@ -10,26 +10,24 @@ import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.Address
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchain.common.address.EstimationFeeAddressFactory
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.blockchain.common.pagination.Page
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.common.trustlines.AssetRequirementsManager
import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.transaction.models.AssetRequirementsCondition
import com.tangem.domain.txhistory.models.PaginationWrapper
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.models.TxHistoryState
@ -49,31 +47,21 @@ import java.util.EnumSet
class DefaultWalletManagersFacade(
private val walletManagersStore: WalletManagersStore,
private val userWalletsStore: UserWalletsStore,
mnemonic: Mnemonic,
assetReader: AssetReader,
moshi: Moshi,
configManager: ConfigManager,
blockchainDataStorage: BlockchainDataStorage,
accountCreator: AccountCreator,
blockchainSDKLogger: BlockchainSDKLogger,
feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles,
blockchainSDKFactory: BlockchainSDKFactory,
) : WalletManagersFacade {
private val demoConfig by lazy { DemoConfig() }
private val resultFactory by lazy { UpdateWalletManagerResultFactory() }
private val walletManagerFactory by lazy {
WalletManagerFactory(
configManager = configManager,
accountCreator = accountCreator,
blockchainDataStorage = blockchainDataStorage,
blockchainSDKLogger = if (feedbackManagerFeatureToggles.isLocalLogsEnabled) blockchainSDKLogger else null,
)
}
private val walletManagerFactory by lazy { WalletManagerFactory(blockchainSDKFactory) }
private val sdkTokenConverter by lazy { SdkTokenConverter() }
private val txHistoryStateConverter by lazy { SdkTransactionHistoryStateConverter() }
private val txHistoryItemConverter by lazy { SdkTransactionHistoryItemConverter(assetReader, moshi) }
private val sdkPageConverter by lazy { SdkPageConverter() }
private val estimationFeeAddressFactory by lazy { EstimationFeeAddressFactory(mnemonic) }
private val cryptoCurrencyTypeConverter by lazy { CryptoCurrencyTypeConverter() }
private val requirementsConditionConverter by lazy { SdkRequirementsConditionConverter() }
private val estimationFeeAddressFactory by lazy { EstimationFeeAddressFactory() }
override suspend fun update(
userWalletId: UserWalletId,
@ -192,7 +180,16 @@ class DefaultWalletManagersFacade(
address = walletManager.wallet.address,
filterType = when (currency) {
is CryptoCurrency.Coin -> TransactionHistoryRequest.FilterType.Coin
is CryptoCurrency.Token -> TransactionHistoryRequest.FilterType.Contract(currency.contractAddress)
is CryptoCurrency.Token -> {
val blockchainToken = Token(
name = currency.name,
symbol = currency.symbol,
contractAddress = currency.contractAddress,
decimals = currency.decimals,
id = currency.id.rawCurrencyId,
)
TransactionHistoryRequest.FilterType.Contract(blockchainToken)
}
},
)
.let(txHistoryStateConverter::convert)
@ -221,7 +218,16 @@ class DefaultWalletManagersFacade(
pageSize = pageSize,
filterType = when (currency) {
is CryptoCurrency.Coin -> TransactionHistoryRequest.FilterType.Coin
is CryptoCurrency.Token -> TransactionHistoryRequest.FilterType.Contract(currency.contractAddress)
is CryptoCurrency.Token -> {
val blockchainToken = Token(
name = currency.name,
symbol = currency.symbol,
contractAddress = currency.contractAddress,
decimals = currency.decimals,
id = currency.id.rawCurrencyId,
)
TransactionHistoryRequest.FilterType.Contract(blockchainToken)
}
},
),
)
@ -573,6 +579,34 @@ class DefaultWalletManagersFacade(
)
}
override suspend fun getAssetRequirements(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): AssetRequirementsCondition? {
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
if (walletManager !is AssetRequirementsManager || !walletManager.hasRequirements(currencyType)) return null
val condition = walletManager.requirementsCondition(currencyType) ?: return null
return requirementsConditionConverter.convert(condition)
}
override suspend fun associateAsset(
userWalletId: UserWalletId,
currency: CryptoCurrency,
signer: CommonSigner,
): SimpleResult {
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
if (walletManager !is AssetRequirementsManager) {
return SimpleResult.Failure(
BlockchainSdkError.CustomError("WalletManager is not implemented AssetRequirementsManager"),
)
}
return walletManager.fulfillRequirements(currencyType, signer)
}
private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set<CryptoCurrency.Token>) {
if (tokens.isEmpty()) return

View file

@ -13,6 +13,7 @@ import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.transaction.models.AssetRequirementsCondition
import com.tangem.domain.txhistory.models.PaginationWrapper
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.models.TxHistoryState
@ -240,4 +241,16 @@ interface WalletManagersFacade {
decimals: Int,
id: String? = null,
): BigDecimal
/**
* Get requirements for asset(currency)
* @return null if there's no requirement, otherwise [AssetRequirementsCondition].
*/
suspend fun getAssetRequirements(userWalletId: UserWalletId, currency: CryptoCurrency): AssetRequirementsCondition?
suspend fun associateAsset(
userWalletId: UserWalletId,
currency: CryptoCurrency,
signer: CommonSigner,
): SimpleResult
}

View file

@ -0,0 +1,23 @@
package com.tangem.domain.walletmanager.utils
import com.tangem.blockchain.common.CryptoCurrencyType
import com.tangem.blockchain.common.Token
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.utils.converter.Converter
internal class CryptoCurrencyTypeConverter : Converter<CryptoCurrency, CryptoCurrencyType> {
override fun convert(value: CryptoCurrency): CryptoCurrencyType {
return when (value) {
is CryptoCurrency.Coin -> CryptoCurrencyType.Coin
is CryptoCurrency.Token -> CryptoCurrencyType.Token(
info = Token(
name = value.name,
symbol = value.symbol,
contractAddress = value.contractAddress,
decimals = value.decimals,
id = value.id.rawCurrencyId,
),
)
}
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.domain.walletmanager.utils
import com.tangem.domain.transaction.models.AssetRequirementsCondition
import com.tangem.utils.converter.Converter
import com.tangem.blockchain.common.trustlines.AssetRequirementsCondition as SdkRequirementsCondition
internal class SdkRequirementsConditionConverter : Converter<SdkRequirementsCondition, AssetRequirementsCondition> {
override fun convert(value: SdkRequirementsCondition): AssetRequirementsCondition {
return when (value) {
SdkRequirementsCondition.PaidTransaction -> AssetRequirementsCondition.PaidTransaction
is SdkRequirementsCondition.PaidTransactionWithFee -> AssetRequirementsCondition.PaidTransactionWithFee(
feeAmount = requireNotNull(value.feeAmount.value),
feeCurrencySymbol = value.feeAmount.currencySymbol,
decimals = value.feeAmount.decimals,
)
}
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.domain.walletmanager.utils
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.txhistory.TransactionHistoryItem
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.utils.converter.Converter
import com.tangem.blockchain.common.txhistory.TransactionHistoryItem as SdkTransactionHistoryItem
@ -58,7 +58,9 @@ internal class SdkTransactionHistoryItemConverter(
} else {
mapToInteractionAddressType(sourceType = sourceType)
}
is SdkTransactionHistoryItem.TransactionType.ContractMethod -> mapToInteractionAddressType(destinationType)
is SdkTransactionHistoryItem.TransactionType.ContractMethod,
is SdkTransactionHistoryItem.TransactionType.ContractMethodName,
-> mapToInteractionAddressType(destinationType)
}
}

View file

@ -4,12 +4,12 @@ import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.blockchain.common.txhistory.TransactionHistoryItem
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.walletmanager.model.SmartContractMethod
import com.tangem.utils.converter.Converter
class SdkTransactionTypeConverter(
internal class SdkTransactionTypeConverter(
private val assetReader: AssetReader,
private val moshi: Moshi,
) : Converter<TransactionHistoryItem.TransactionType, TxHistoryItem.TransactionType> {
@ -27,16 +27,20 @@ class SdkTransactionTypeConverter(
override fun convert(value: TransactionHistoryItem.TransactionType): TxHistoryItem.TransactionType {
return when (value) {
TransactionHistoryItem.TransactionType.Transfer -> TxHistoryItem.TransactionType.Transfer
is TransactionHistoryItem.TransactionType.ContractMethod -> {
return when (val name = smartContractMethods[value.id]?.name) {
"transfer" -> TxHistoryItem.TransactionType.Transfer
"approve" -> TxHistoryItem.TransactionType.Approve
"swap" -> TxHistoryItem.TransactionType.Swap
null -> TxHistoryItem.TransactionType.UnknownOperation
else -> TxHistoryItem.TransactionType.Operation(name = name.replaceFirstChar { it.titlecase() })
}
}
is TransactionHistoryItem.TransactionType.ContractMethod ->
getTransactionType(methodName = smartContractMethods[value.id]?.name)
is TransactionHistoryItem.TransactionType.ContractMethodName -> getTransactionType(methodName = value.name)
is TransactionHistoryItem.TransactionType.Transfer -> TxHistoryItem.TransactionType.Transfer
}
}
private fun getTransactionType(methodName: String?): TxHistoryItem.TransactionType {
return when (methodName) {
"transfer" -> TxHistoryItem.TransactionType.Transfer
"approve" -> TxHistoryItem.TransactionType.Approve
"swap" -> TxHistoryItem.TransactionType.Swap
null -> TxHistoryItem.TransactionType.UnknownOperation
else -> TxHistoryItem.TransactionType.Operation(name = methodName.replaceFirstChar { it.titlecase() })
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.domain.walletmanager.utils
import com.tangem.blockchain.common.*
import com.tangem.domain.common.extensions.amountToCreateAccount
import com.tangem.blockchainsdk.utils.amountToCreateAccount
import com.tangem.domain.walletmanager.model.Address
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction

View file

@ -1,37 +1,21 @@
package com.tangem.domain.walletmanager.utils
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationParams
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.makeWalletManagerForApp
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
import timber.log.Timber
import com.tangem.blockchain.common.WalletManagerFactory as BlockchainWalletManagerFactory
internal class WalletManagerFactory(
configManager: ConfigManager,
accountCreator: AccountCreator,
blockchainDataStorage: BlockchainDataStorage,
blockchainSDKLogger: BlockchainSDKLogger? = null,
private val blockchainSDKFactory: BlockchainSDKFactory,
) {
private val sdkWalletManagerFactory by lazy {
BlockchainWalletManagerFactory(
config = configManager.config.blockchainSdkConfig,
accountCreator = accountCreator,
blockchainDataStorage = blockchainDataStorage,
loggers = listOfNotNull(blockchainSDKLogger),
)
}
fun createWalletManager(
suspend fun createWalletManager(
scanResponse: ScanResponse,
blockchain: Blockchain,
derivationPath: DerivationPath?,
@ -39,7 +23,7 @@ internal class WalletManagerFactory(
val derivationParams = getDerivationParams(derivationPath, scanResponse.derivationStyleProvider)
return try {
sdkWalletManagerFactory.makeWalletManagerForApp(
blockchainSDKFactory.getWalletManagerFactorySync()?.makeWalletManagerForApp(
scanResponse = scanResponse,
blockchain = blockchain,
derivationParams = derivationParams,

View file

@ -2,14 +2,14 @@ package com.tangem.domain.features
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toNetworkId
import org.junit.Test
class BlockchainTests {
@Test
fun allNetworkIdsAreImplemented() {
val unimplementedIds = Blockchain.values()
val unimplementedIds = Blockchain.entries
.toMutableList()
.apply {
remove(Blockchain.Unknown)

View file

@ -0,0 +1,13 @@
package com.tangem.domain.settings
import arrow.core.Either
import com.tangem.domain.settings.repositories.SettingsRepository
class IncrementAppLaunchCounterUseCase(
private val settingsRepository: SettingsRepository,
) {
suspend operator fun invoke(): Either<Throwable, Unit> {
return Either.catch { settingsRepository.incrementAppLaunchCounter() }
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.settings
import arrow.core.Either
import com.tangem.domain.settings.repositories.SettingsRepository
class SetSaveWalletScreenShownUseCase(
private val settingsRepository: SettingsRepository,
) {
suspend operator fun invoke(): Either<Throwable, Unit> {
return Either.catch {
settingsRepository.setShouldShowSaveUserWalletScreen(value = false)
}
}
}

View file

@ -4,6 +4,8 @@ interface SettingsRepository {
suspend fun shouldShowSaveUserWalletScreen(): Boolean
suspend fun setShouldShowSaveUserWalletScreen(value: Boolean)
suspend fun isWalletScrollPreviewEnabled(): Boolean
suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean)
@ -17,4 +19,18 @@ interface SettingsRepository {
suspend fun isSendTapHelpPreviewEnabled(): Boolean
suspend fun setSendTapHelpPreviewAvailability(isEnabled: Boolean)
suspend fun wasApplicationStopped(): Boolean
suspend fun setWasApplicationStopped(value: Boolean)
suspend fun shouldOpenWelcomeScreenOnResume(): Boolean
suspend fun setShouldOpenWelcomeScreenOnResume(value: Boolean)
suspend fun shouldSaveAccessCodes(): Boolean
suspend fun setShouldSaveAccessCodes(value: Boolean)
suspend fun incrementAppLaunchCounter()
}

View file

@ -11,11 +11,13 @@ android {
dependencies {
/** Project - Domain */
implementation(projects.domain.core)
api(projects.domain.core)
implementation(projects.domain.models)
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.tokens.models)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.transaction.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.settings)

View file

@ -15,7 +15,7 @@ import java.math.BigDecimal
*/
data class CryptoCurrencyStatus(
val currency: CryptoCurrency,
val value: Status,
val value: Value,
) {
/**
@ -23,7 +23,7 @@ data class CryptoCurrencyStatus(
*
* @property isError Indicates whether this status represents an error status.
*/
sealed class Status(val isError: Boolean) {
sealed class Value(val isError: Boolean) {
/** The amount of the cryptocurrency. */
open val amount: BigDecimal? = null
@ -48,7 +48,7 @@ data class CryptoCurrencyStatus(
}
/** Represents the Loading state of a cryptocurrency, typically while fetching its details. */
object Loading : Status(isError = false)
data object Loading : Value(isError = false)
/**
* Represents a state where the cryptocurrency is not reachable.
@ -61,19 +61,19 @@ data class CryptoCurrencyStatus(
override val priceChange: BigDecimal?,
override val fiatRate: BigDecimal?,
override val networkAddress: NetworkAddress?,
) : Status(isError = true)
) : Value(isError = true)
/** Represents a state where the cryptocurrency's network amount not found. */
data class NoAmount(
override val priceChange: BigDecimal?,
override val fiatRate: BigDecimal?,
) : Status(isError = true)
) : Value(isError = true)
/** Represents a state where the cryptocurrency's derivation is missed. */
data class MissedDerivation(
override val priceChange: BigDecimal?,
override val fiatRate: BigDecimal?,
) : Status(isError = true)
) : Value(isError = true)
/**
* Represents a state where there is no account associated with the cryptocurrency
@ -86,7 +86,7 @@ data class CryptoCurrencyStatus(
override val priceChange: BigDecimal?,
override val fiatRate: BigDecimal?,
override val networkAddress: NetworkAddress,
) : Status(isError = false) {
) : Value(isError = false) {
override val amount: BigDecimal = BigDecimal.ZERO
}
@ -110,7 +110,7 @@ data class CryptoCurrencyStatus(
override val hasCurrentNetworkTransactions: Boolean,
override val pendingTransactions: Set<TxHistoryItem>,
override val networkAddress: NetworkAddress,
) : Status(isError = false)
) : Value(isError = false)
/**
* Represents a Custom state of a cryptocurrency, typically used for user-defined tokens.
@ -131,7 +131,7 @@ data class CryptoCurrencyStatus(
override val hasCurrentNetworkTransactions: Boolean,
override val pendingTransactions: Set<TxHistoryItem>,
override val networkAddress: NetworkAddress,
) : Status(isError = false)
) : Value(isError = false)
/**
* Represents a state where the cryptocurrency is available, but there is no current quote available for it.
@ -146,5 +146,5 @@ data class CryptoCurrencyStatus(
override val hasCurrentNetworkTransactions: Boolean,
override val pendingTransactions: Set<TxHistoryItem>,
override val networkAddress: NetworkAddress,
) : Status(isError = false)
) : Value(isError = false)
}

View file

@ -11,7 +11,7 @@ import java.math.BigDecimal
*/
data class NetworkStatus(
val network: Network,
val value: Status,
val value: Value,
) {
/**
@ -19,19 +19,19 @@ data class NetworkStatus(
*
* This sealed class includes different states like unreachable, missed derivation, verified, and no account.
*/
sealed class Status
sealed class Value
/**
* Represents the state where the network is unreachable.
*
* @property address Network addresses.
*/
data class Unreachable(val address: NetworkAddress?) : Status()
data class Unreachable(val address: NetworkAddress?) : Value()
/**
* Represents the state where a derivation has been missed.
*/
object MissedDerivation : Status()
data object MissedDerivation : Value()
/**
* Represents the verified state of the network, including the amounts associated with different cryptocurrencies
@ -46,7 +46,7 @@ data class NetworkStatus(
val address: NetworkAddress,
val amounts: Map<CryptoCurrency.ID, CryptoCurrencyAmountStatus>,
val pendingTransactions: Map<CryptoCurrency.ID, Set<TxHistoryItem>>,
) : Status()
) : Value()
/**
* Represents the state where there is no account, and an amount is required to create one.
@ -59,5 +59,5 @@ data class NetworkStatus(
val address: NetworkAddress,
val amountToCreateAccount: BigDecimal,
val errorMessage: String,
) : Status()
) : Value()
}

View file

@ -31,7 +31,7 @@ sealed class CryptoCurrencyWarning {
val amountCurrency: CryptoCurrency,
) : CryptoCurrencyWarning()
object TopUpWithoutReserve : CryptoCurrencyWarning()
data object TopUpWithoutReserve : CryptoCurrencyWarning()
/**
* Represents wallet blockchain rent
@ -41,8 +41,6 @@ sealed class CryptoCurrencyWarning {
*/
data class Rent(val rent: BigDecimal, val exemptionAmount: BigDecimal) : CryptoCurrencyWarning()
data class HasPendingTransactions(val blockchainSymbol: String) : CryptoCurrencyWarning()
data class SwapPromo(
val startDateTime: DateTime,
val endDateTime: DateTime,

View file

@ -0,0 +1,18 @@
package com.tangem.domain.tokens.model.warnings
import com.tangem.domain.tokens.model.CryptoCurrency
import java.math.BigDecimal
sealed class HederaWarnings : CryptoCurrencyWarning() {
abstract val currency: CryptoCurrency
data class AssociateWarning(override val currency: CryptoCurrency) : HederaWarnings()
data class AssociateWarningWithFee(
override val currency: CryptoCurrency,
val fee: BigDecimal,
val feeCurrencySymbol: String,
val feeCurrencyDecimals: Int,
) : HederaWarnings()
}

View file

@ -72,4 +72,9 @@ sealed class TokenScreenAnalyticsEvent(
event = "Token Bought",
params = mapOf("Token" to token),
)
class Associate(tokenSymbol: String, blockchain: String) : TokenScreenAnalyticsEvent(
event = "Button - Token Trustline",
params = mapOf("Token" to tokenSymbol, "Blockchain" to blockchain),
)
}

View file

@ -1,7 +1,7 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.left
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.error.mapper.mapToTokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -12,19 +12,19 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transformLatest
class GetCardTokensListUseCase(
internal val currenciesRepository: CurrenciesRepository,
internal val quotesRepository: QuotesRepository,
internal val networksRepository: NetworksRepository,
internal val dispatchers: CoroutineDispatcherProvider,
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
) {
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(userWalletId: UserWalletId): Flow<Either<TokenListError, TokenList>> {
operator fun invoke(userWalletId: UserWalletId): EitherFlow<TokenListError, TokenList> {
return getTokensStatuses(userWalletId).transformLatest { maybeTokens ->
maybeTokens.fold(
ifLeft = { error ->
@ -37,9 +37,7 @@ class GetCardTokensListUseCase(
}
}
private fun getTokensStatuses(
userWalletId: UserWalletId,
): Flow<Either<TokenListError, List<CryptoCurrencyStatus>>> {
private fun getTokensStatuses(userWalletId: UserWalletId): EitherFlow<TokenListError, List<CryptoCurrencyStatus>> {
val operations = CurrenciesStatusesOperations(
userWalletId = userWalletId,
currenciesRepository = currenciesRepository,
@ -56,7 +54,7 @@ class GetCardTokensListUseCase(
private fun createTokenList(
userWalletId: UserWalletId,
tokens: List<CryptoCurrencyStatus>,
): Flow<Either<TokenListError, TokenList>> {
): EitherFlow<TokenListError, TokenList> {
val operations = TokenListOperations(
userWalletId = userWalletId,
tokens = tokens,

View file

@ -8,14 +8,12 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import java.math.BigDecimal
/**
* Use case to determine which TokenActions are available for a [CryptoCurrency]
@ -25,16 +23,19 @@ import java.math.BigDecimal
@Suppress("LongParameterList")
class GetCryptoCurrencyActionsUseCase(
private val rampManager: RampStateManager,
private val walletManagersFacade: WalletManagersFacade,
private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val sendFeatureToggles: SendFeatureToggles,
private val dispatchers: CoroutineDispatcherProvider,
) {
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus): Flow<TokenActionsState> {
suspend operator fun invoke(
userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): Flow<TokenActionsState> {
val operations = CurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
@ -42,7 +43,7 @@ class GetCryptoCurrencyActionsUseCase(
userWalletId = userWallet.walletId,
)
val networkId = cryptoCurrencyStatus.currency.network.id
val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, cryptoCurrencyStatus.currency)
return flow {
val networkFlow = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId)
@ -57,6 +58,7 @@ class GetCryptoCurrencyActionsUseCase(
userWallet = userWallet,
coinStatus = maybeCoinStatus.getOrNull(),
cryptoCurrencyStatus = cryptoCurrencyStatus,
needAssociateAsset = requirements != null,
)
}
@ -68,33 +70,37 @@ class GetCryptoCurrencyActionsUseCase(
userWallet: UserWallet,
coinStatus: CryptoCurrencyStatus?,
cryptoCurrencyStatus: CryptoCurrencyStatus,
needAssociateAsset: Boolean,
): TokenActionsState {
return TokenActionsState(
walletId = userWallet.walletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
states = createListOfActions(
userWallet,
coinStatus,
cryptoCurrencyStatus,
userWallet = userWallet,
coinStatus = coinStatus,
cryptoCurrencyStatus = cryptoCurrencyStatus,
needAssociateAsset = needAssociateAsset,
),
)
}
/**
* Creates list of action for expected order
* Actions priority: [Buy Send Receive Sell Swap]
* Actions priority: [Receive Send Swap Buy Sell]
*/
@Suppress("CyclomaticComplexMethod", "LongMethod")
private suspend fun createListOfActions(
userWallet: UserWallet,
coinStatus: CryptoCurrencyStatus?,
cryptoCurrencyStatus: CryptoCurrencyStatus,
needAssociateAsset: Boolean,
): List<TokenActionsState.ActionState> {
val cryptoCurrency = cryptoCurrencyStatus.currency
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation) {
return listOf(TokenActionsState.ActionState.HideToken(true))
return listOf(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None))
}
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Unreachable) {
return getActionsForUnreachableCurrency(cryptoCurrencyStatus)
return getActionsForUnreachableCurrency(cryptoCurrencyStatus, needAssociateAsset)
}
val activeList = mutableListOf<TokenActionsState.ActionState>()
@ -102,115 +108,155 @@ class GetCryptoCurrencyActionsUseCase(
// copy address
if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) {
activeList.add(TokenActionsState.ActionState.CopyAddress(true))
activeList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None))
}
// receive
if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) {
activeList.add(TokenActionsState.ActionState.Receive(true))
val scenario = if (needAssociateAsset) {
ScenarioUnavailabilityReason.UnassociatedAsset
} else {
ScenarioUnavailabilityReason.None
}
activeList.add(TokenActionsState.ActionState.Receive(scenario))
}
// send
if (
isSendDisabled(
userWalletId = userWallet.walletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
coinStatus = coinStatus,
)
) {
disabledList.add(TokenActionsState.ActionState.Send(false))
val sendUnavailabilityReason = getSendUnavailabilityReason(
cryptoCurrencyStatus = cryptoCurrencyStatus,
coinStatus = coinStatus,
)
if (sendUnavailabilityReason == ScenarioUnavailabilityReason.None) {
activeList.add(TokenActionsState.ActionState.Send(sendUnavailabilityReason))
} else {
activeList.add(TokenActionsState.ActionState.Send(true))
disabledList.add(TokenActionsState.ActionState.Send(sendUnavailabilityReason))
}
// swap
if (userWallet.isMultiCurrency) {
if (marketCryptoCurrencyRepository.isExchangeable(userWallet.walletId, cryptoCurrency)) {
activeList.add(TokenActionsState.ActionState.Swap(true))
if (
marketCryptoCurrencyRepository.isExchangeable(userWallet.walletId, cryptoCurrency) &&
cryptoCurrencyStatus.value !is CryptoCurrencyStatus.NoQuote
) {
activeList.add(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None))
} else {
disabledList.add(TokenActionsState.ActionState.Swap(false))
disabledList.add(
TokenActionsState.ActionState.Swap(
unavailabilityReason = ScenarioUnavailabilityReason.NotExchangeable(cryptoCurrency.name),
),
)
}
}
// buy
if (rampManager.availableForBuy(cryptoCurrency)) {
activeList.add(TokenActionsState.ActionState.Buy(true))
activeList.add(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None))
} else {
disabledList.add(TokenActionsState.ActionState.Buy(false))
disabledList.add(
TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.BuyUnavailable(cryptoCurrency.symbol)),
)
}
// sell
if (rampManager.availableForSell(cryptoCurrency)) {
activeList.add(TokenActionsState.ActionState.Sell(true))
} else {
disabledList.add(TokenActionsState.ActionState.Sell(false))
val sellSupportedByService = rampManager.availableForSell(cryptoCurrency)
val sendAvailable = sendUnavailabilityReason is ScenarioUnavailabilityReason.None
when {
sellSupportedByService && sendAvailable -> {
activeList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None))
}
sellSupportedByService && !sendAvailable -> {
(sendUnavailabilityReason as? ScenarioUnavailabilityReason.EmptyBalance)?.let {
disabledList.add(
TokenActionsState.ActionState.Sell(
unavailabilityReason = it.copy(
withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SELL,
),
),
)
}
(sendUnavailabilityReason as? ScenarioUnavailabilityReason.PendingTransaction)?.let {
disabledList.add(
TokenActionsState.ActionState.Sell(
unavailabilityReason = it.copy(
withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SELL,
),
),
)
}
}
else -> {
disabledList.add(
TokenActionsState.ActionState.Sell(
unavailabilityReason = ScenarioUnavailabilityReason.NotSupportedBySellService(
cryptoCurrency.name,
),
),
)
}
}
// hide
activeList.add(TokenActionsState.ActionState.HideToken(true))
activeList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None))
return activeList + disabledList
}
private fun getActionsForUnreachableCurrency(
cryptoCurrencyStatus: CryptoCurrencyStatus,
needAssociateAsset: Boolean,
): List<TokenActionsState.ActionState> {
val activeList = mutableListOf<TokenActionsState.ActionState>()
val disabledList = mutableListOf<TokenActionsState.ActionState>()
val actionsList = mutableListOf<TokenActionsState.ActionState>()
if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) {
activeList.add(TokenActionsState.ActionState.CopyAddress(true))
actionsList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None))
}
if (rampManager.availableForBuy(cryptoCurrencyStatus.currency)) {
activeList.add(TokenActionsState.ActionState.Buy(true))
actionsList.add(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None))
} else {
disabledList.add(TokenActionsState.ActionState.Buy(false))
actionsList.add(
TokenActionsState.ActionState.Buy(
ScenarioUnavailabilityReason.BuyUnavailable(
cryptoCurrencyName = cryptoCurrencyStatus.currency.name,
),
),
)
}
disabledList.add(TokenActionsState.ActionState.Send(false))
disabledList.add(TokenActionsState.ActionState.Swap(false))
disabledList.add(TokenActionsState.ActionState.Sell(false))
actionsList.add(TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.Unreachable))
actionsList.add(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.Unreachable))
actionsList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.Unreachable))
if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) {
activeList.add(TokenActionsState.ActionState.Receive(true))
val scenario = if (needAssociateAsset) {
ScenarioUnavailabilityReason.UnassociatedAsset
} else {
ScenarioUnavailabilityReason.None
}
actionsList.add(TokenActionsState.ActionState.Receive(scenario))
}
activeList.add(TokenActionsState.ActionState.HideToken(true))
return activeList + disabledList
actionsList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None))
return actionsList
}
private suspend fun isSendDisabled(
userWalletId: UserWalletId,
private fun getSendUnavailabilityReason(
cryptoCurrencyStatus: CryptoCurrencyStatus,
coinStatus: CryptoCurrencyStatus?,
): Boolean {
val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, cryptoCurrencyStatus.currency)
val notEnoughBalanceForFee = isNotEnoughBalanceForFee(
feePaidCurrency = feePaidCurrency,
tokenStatus = cryptoCurrencyStatus,
coinStatus = coinStatus,
)
return cryptoCurrencyStatus.value.amount.isNullOrZero() ||
notEnoughBalanceForFee ||
): ScenarioUnavailabilityReason {
return when {
cryptoCurrencyStatus.value.amount.isNullOrZero() -> {
ScenarioUnavailabilityReason.EmptyBalance(ScenarioUnavailabilityReason.WithdrawalScenario.SEND)
}
currenciesRepository.hasPendingTransactions(
cryptoCurrencyStatus = cryptoCurrencyStatus,
coinStatus = coinStatus,
)
}
private fun isNotEnoughBalanceForFee(
feePaidCurrency: FeePaidCurrency,
tokenStatus: CryptoCurrencyStatus,
coinStatus: CryptoCurrencyStatus?,
): Boolean {
return if (sendFeatureToggles.isRedesignedSendEnabled) {
tokenStatus.value.amount.isZero()
} else {
when (feePaidCurrency) {
FeePaidCurrency.Coin -> !tokenStatus.value.amount.isZero() && coinStatus?.value?.amount.isZero()
FeePaidCurrency.SameCurrency -> tokenStatus.value.amount.isZero()
is FeePaidCurrency.Token -> {
val feePaidTokenBalance = feePaidCurrency.balance
!tokenStatus.value.amount.isZero() && feePaidTokenBalance.isZero()
}
) -> {
ScenarioUnavailabilityReason.PendingTransaction(
withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SEND,
networkName = coinStatus?.currency?.network?.name.orEmpty(),
)
}
else -> {
ScenarioUnavailabilityReason.None
}
}
}
@ -218,8 +264,4 @@ class GetCryptoCurrencyActionsUseCase(
private fun isAddressAvailable(networkAddress: NetworkAddress?): Boolean {
return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty()
}
private fun BigDecimal?.isZero(): Boolean {
return this?.signum() == 0
}
}

View file

@ -6,8 +6,10 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.FeePaidCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.tokens.model.warnings.HederaWarnings
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.transaction.models.AssetRequirementsCondition
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.api.SwapRepository
@ -76,6 +78,7 @@ class GetCurrencyWarningsUseCase(
getNetworkUnavailableWarning(currencyStatus),
getNetworkNoAccountWarning(currencyStatus),
getBeaconChainShutdownWarning(currency.network.id),
getAssetRequirementsWarning(userWalletId = userWalletId, currency = currency),
)
}.flowOn(dispatchers.io)
}
@ -169,9 +172,6 @@ class GetCurrencyWarningsUseCase(
when {
tokenStatus != null && coinStatus != null -> {
buildList {
if (currenciesRepository.hasPendingTransactions(tokenStatus, coinStatus)) {
add(CryptoCurrencyWarning.HasPendingTransactions(coinStatus.currency.symbol))
}
getFeeWarning(
userWalletId = userWalletId,
coinStatus = coinStatus,
@ -270,6 +270,22 @@ class GetCurrencyWarningsUseCase(
return if (BlockchainUtils.isBeaconChain(networkId.value)) CryptoCurrencyWarning.BeaconChainShutdown else null
}
private suspend fun getAssetRequirementsWarning(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): CryptoCurrencyWarning? {
return when (val requirements = walletManagersFacade.getAssetRequirements(userWalletId, currency)) {
is AssetRequirementsCondition.PaidTransaction -> HederaWarnings.AssociateWarning(currency = currency)
is AssetRequirementsCondition.PaidTransactionWithFee -> HederaWarnings.AssociateWarningWithFee(
currency = currency,
fee = requirements.feeAmount,
feeCurrencySymbol = requirements.feeCurrencySymbol,
feeCurrencyDecimals = requirements.decimals,
)
null -> null
}
}
private fun BigDecimal?.isZero(): Boolean {
return this?.signum() == 0
}

View file

@ -1,37 +1,46 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.left
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.core.utils.toLce
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.error.mapper.mapToTokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.operations.CurrenciesStatusesLceOperations
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
import com.tangem.domain.tokens.operations.TokenListOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transformLatest
class GetTokenListUseCase(
internal val currenciesRepository: CurrenciesRepository,
internal val quotesRepository: QuotesRepository,
internal val networksRepository: NetworksRepository,
internal val dispatchers: CoroutineDispatcherProvider,
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
) {
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(userWalletId: UserWalletId): Flow<Either<TokenListError, TokenList>> {
return getTokensStatuses(userWalletId).transformLatest { maybeTokens ->
fun launch(userWalletId: UserWalletId): EitherFlow<TokenListError, TokenList> {
val operations = CurrenciesStatusesOperations(
userWalletId = userWalletId,
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
)
return operations.getCurrenciesStatusesFlow().transformLatest { maybeTokens ->
maybeTokens.fold(
ifLeft = { error ->
emit(error.left())
emit(error.mapToTokenListError().left())
},
ifRight = { tokens ->
emitAll(createTokenList(userWalletId, tokens))
@ -40,32 +49,61 @@ class GetTokenListUseCase(
}
}
private fun getTokensStatuses(
userWalletId: UserWalletId,
): Flow<Either<TokenListError, List<CryptoCurrencyStatus>>> {
val operations = CurrenciesStatusesOperations(
userWalletId = userWalletId,
useCase = this@GetTokenListUseCase,
@OptIn(ExperimentalCoroutinesApi::class)
fun launchLce(userWalletId: UserWalletId): LceFlow<TokenListError, TokenList> {
val operations = CurrenciesStatusesLceOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
)
return operations.getCurrenciesStatusesFlow()
.map { maybeCurrenciesStatuses ->
maybeCurrenciesStatuses.mapLeft(CurrenciesStatusesOperations.Error::mapToTokenListError)
}
return operations.getCurrenciesStatuses(userWalletId).transformLatest { maybeCurrencies ->
maybeCurrencies.fold(
ifLoading = { maybeContent ->
if (maybeContent != null) {
emitAll(createTokenListLce(userWalletId, maybeContent, isCurrenciesLoading = true))
} else {
emit(lceLoading())
}
},
ifContent = { content ->
emitAll(createTokenListLce(userWalletId, content, isCurrenciesLoading = false))
},
ifError = { error -> emit(error.lceError()) },
)
}
}
private fun createTokenList(
userWalletId: UserWalletId,
tokens: List<CryptoCurrencyStatus>,
): Flow<Either<TokenListError, TokenList>> {
): EitherFlow<TokenListError, TokenList> {
val operations = TokenListOperations(
userWalletId = userWalletId,
tokens = tokens,
useCase = this@GetTokenListUseCase,
currenciesRepository = currenciesRepository,
)
return operations.getTokenListFlow().map { maybeTokenList ->
maybeTokenList.mapLeft(TokenListOperations.Error::mapToTokenListError)
}
}
private fun createTokenListLce(
userWalletId: UserWalletId,
currencies: List<CryptoCurrencyStatus>,
isCurrenciesLoading: Boolean,
): LceFlow<TokenListError, TokenList> {
val operations = TokenListOperations(
userWalletId = userWalletId,
tokens = currencies,
currenciesRepository = currenciesRepository,
)
return operations.getTokenListFlow().map { maybeTokenList ->
maybeTokenList
.mapLeft(TokenListOperations.Error::mapToTokenListError)
.toLce(isCurrenciesLoading)
}
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.domain.tokens.model
sealed class ScenarioUnavailabilityReason {
data object None : ScenarioUnavailabilityReason()
// send&sell-specific
data class PendingTransaction(
val withdrawalScenario: WithdrawalScenario,
val networkName: String,
) : ScenarioUnavailabilityReason()
data class EmptyBalance(val withdrawalScenario: WithdrawalScenario) : ScenarioUnavailabilityReason()
// buy-specific
data class BuyUnavailable(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason()
// swap-specific
data class NotExchangeable(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason()
// sell-specific
data class NotSupportedBySellService(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason()
data object Unreachable : ScenarioUnavailabilityReason()
data object UnassociatedAsset : ScenarioUnavailabilityReason()
enum class WithdrawalScenario {
SELL, SEND
}
}

View file

@ -10,20 +10,20 @@ data class TokenActionsState(
sealed class ActionState {
abstract val enabled: Boolean
abstract val unavailabilityReason: ScenarioUnavailabilityReason
data class Buy(override val enabled: Boolean) : ActionState()
data class Buy(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState()
data class CopyAddress(override val enabled: Boolean) : ActionState()
data class CopyAddress(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState()
data class Sell(override val enabled: Boolean) : ActionState()
data class Sell(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState()
data class Receive(override val enabled: Boolean) : ActionState()
data class Receive(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState()
data class Swap(override val enabled: Boolean) : ActionState()
data class Swap(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState()
data class Send(override val enabled: Boolean) : ActionState()
data class Send(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState()
data class HideToken(override val enabled: Boolean) : ActionState()
data class HideToken(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState()
}
}

View file

@ -0,0 +1,153 @@
package com.tangem.domain.tokens.operations
import arrow.core.*
import arrow.core.raise.recover
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.lce.lce
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.*
internal class CurrenciesStatusesLceOperations(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
) {
fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow<TokenListError, List<CryptoCurrencyStatus>> {
return getMultiCurrencyWalletCurrencies(userWalletId).transform transform@{ maybeCurrencies ->
val nonEmptyCurrencies = maybeCurrencies.fold(
ifLoading = { maybeContent ->
emit(createLoadingCurrenciesStatuses(maybeContent))
return@transform
},
ifContent = { content ->
val nonEmptyCurrencies = content.toNonEmptyListOrNull()
if (nonEmptyCurrencies == null) {
emit(TokenListError.EmptyTokens.lceError())
return@transform
} else {
nonEmptyCurrencies
}
},
ifError = { error ->
emit(error.lceError())
return@transform
},
)
val (networks, currenciesIds) = getIds(nonEmptyCurrencies)
combine(
getQuotes(currenciesIds),
getNetworksStatuses(userWalletId, networks),
) { maybeQuotes, maybeNetworksStatuses ->
val statuses = createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses)
emit(statuses)
}.collect()
}
}
private fun createLoadingCurrenciesStatuses(
maybeCurrencies: List<CryptoCurrency>?,
): Lce<TokenListError, List<CryptoCurrencyStatus>> {
val nonEmptyCurrencies = maybeCurrencies?.toNonEmptyListOrNull()
val statuses = if (nonEmptyCurrencies == null) {
lceLoading()
} else {
createCurrenciesStatuses(
nonEmptyCurrencies,
maybeNetworkStatuses = null,
maybeQuotes = null,
)
}
return statuses
}
private fun getMultiCurrencyWalletCurrencies(
userWalletId: UserWalletId,
): LceFlow<TokenListError, List<CryptoCurrency>> {
return currenciesRepository.getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId)
.map { maybeCurrencies ->
maybeCurrencies.mapError { TokenListError.DataError(it) }
}
}
private fun createCurrenciesStatuses(
currencies: NonEmptyList<CryptoCurrency>,
maybeQuotes: Either<TokenListError, Set<Quote>>?,
maybeNetworkStatuses: Lce<TokenListError, Set<NetworkStatus>>?,
): Lce<TokenListError, List<CryptoCurrencyStatus>> = lce {
isLoading.set(maybeNetworkStatuses == null)
var quotesRetrievingFailed = false
val networksStatuses = maybeNetworkStatuses?.bindOrNull()?.toNonEmptySetOrNull()
val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) {
quotesRetrievingFailed = true
null
}
currencies.map { currency ->
val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId }
val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network }
createCurrencyStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed)
}
}
private fun createCurrencyStatus(
currency: CryptoCurrency,
quote: Quote?,
networkStatus: NetworkStatus?,
ignoreQuote: Boolean,
): CryptoCurrencyStatus {
val currencyStatusOperations = CurrencyStatusOperations(
currency = currency,
quote = quote,
networkStatus = networkStatus,
ignoreQuote = ignoreQuote,
)
return currencyStatusOperations.createTokenStatus()
}
private fun getQuotes(tokensIds: NonEmptySet<CryptoCurrency.ID>): Flow<Either<TokenListError, Set<Quote>>> {
return quotesRepository.getQuotesUpdates(tokensIds)
.map<Set<Quote>, Either<TokenListError, Set<Quote>>> { it.right() }
.catch { emit(TokenListError.DataError(it).left()) }
}
private fun getNetworksStatuses(
userWalletId: UserWalletId,
networks: NonEmptySet<Network>,
): LceFlow<TokenListError, Set<NetworkStatus>> {
return networksRepository.getNetworkStatusesUpdatesLce(userWalletId, networks)
.map { maybeStatuses ->
maybeStatuses.mapError { TokenListError.DataError(it) }
}
}
private fun getIds(currencies: List<CryptoCurrency>): Pair<NonEmptySet<Network>, NonEmptySet<CryptoCurrency.ID>> {
val currencyIdToNetworkId = currencies.associate { currency ->
currency.id to currency.network
}
val currenciesIds = currencyIdToNetworkId.keys.toNonEmptySetOrNull()
val networks = currencyIdToNetworkId.values.toNonEmptySetOrNull()
requireNotNull(currenciesIds) { "Currencies IDs cannot be empty" }
requireNotNull(networks) { "Networks IDs cannot be empty" }
return networks to currenciesIds
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.domain.tokens.operations
import arrow.core.*
import arrow.core.raise.*
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
@ -20,18 +20,8 @@ internal class CurrenciesStatusesOperations(
private val userWalletId: UserWalletId,
) {
constructor(
userWalletId: UserWalletId,
useCase: GetTokenListUseCase,
) : this(
currenciesRepository = useCase.currenciesRepository,
quotesRepository = useCase.quotesRepository,
networksRepository = useCase.networksRepository,
userWalletId = userWalletId,
)
@OptIn(ExperimentalCoroutinesApi::class)
fun getCurrenciesStatusesFlow(): Flow<Either<Error, List<CryptoCurrencyStatus>>> {
fun getCurrenciesStatusesFlow(): EitherFlow<Error, List<CryptoCurrencyStatus>> {
return getMultiCurrencyWalletCurrencies().transformLatest { maybeCurrencies ->
val nonEmptyCurrencies = maybeCurrencies.fold(
ifLeft = { error ->

View file

@ -12,7 +12,7 @@ internal class CurrencyStatusOperations(
fun createTokenStatus(): CryptoCurrencyStatus = CryptoCurrencyStatus(currency, createStatus())
private fun createStatus(): CryptoCurrencyStatus.Status {
private fun createStatus(): CryptoCurrencyStatus.Value {
return when (val status = networkStatus?.value) {
null -> CryptoCurrencyStatus.Loading
is NetworkStatus.MissedDerivation -> createMissedDerivationStatus()
@ -42,7 +42,7 @@ internal class CurrencyStatusOperations(
networkAddress = status.address,
)
private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Status {
private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Value {
val amount = when (val amount = status.amounts[currency.id]) {
null -> {
return CryptoCurrencyStatus.Loading

View file

@ -4,7 +4,6 @@ import arrow.core.*
import arrow.core.raise.Raise
import arrow.core.raise.either
import arrow.core.raise.withError
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -18,16 +17,6 @@ internal class TokenListOperations(
private val tokens: List<CryptoCurrencyStatus>,
) {
constructor(
userWalletId: UserWalletId,
tokens: List<CryptoCurrencyStatus>,
useCase: GetTokenListUseCase,
) : this(
currenciesRepository = useCase.currenciesRepository,
userWalletId = userWalletId,
tokens = tokens,
)
fun getTokenListFlow(): Flow<Either<Error, TokenList>> {
return combine(
getIsGrouped(),

View file

@ -1,5 +1,7 @@
package com.tangem.domain.tokens.repository
import com.tangem.domain.core.error.DataError
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.FeePaidCurrency
@ -20,7 +22,7 @@ interface CurrenciesRepository {
* @param currencies The list of cryptocurrencies to be saved.
* @param isGroupedByNetwork A boolean flag indicating whether the tokens should be grouped by network.
* @param isSortedByBalance A boolean flag indicating whether the tokens should be sorted by balance.
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
suspend fun saveTokens(
@ -35,7 +37,7 @@ interface CurrenciesRepository {
*
* @param userWalletId The unique identifier of the user wallet.
* @param currencies The currencies which must be added.
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>)
@ -45,7 +47,7 @@ interface CurrenciesRepository {
*
* @param userWalletId The unique identifier of the user wallet.
* @param currency The currency which must be removed.
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If multi-currency user wallet
* @throws DataError.UserWalletError.WrongUserWallet If multi-currency user wallet
* ID provided.
*/
suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency)
@ -55,7 +57,7 @@ interface CurrenciesRepository {
*
* @param userWalletId The unique identifier of the user wallet.
* @param currencies The currencies which must be removed.
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>)
@ -65,7 +67,7 @@ interface CurrenciesRepository {
*
* @param userWalletId The unique identifier of the user wallet.
* @return The primary cryptocurrency associated with the user wallet.
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If multi-currency user wallet
* @throws DataError.UserWalletError.WrongUserWallet If multi-currency user wallet
* ID provided.
*/
suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency
@ -75,7 +77,7 @@ interface CurrenciesRepository {
*
* @param userWalletId The unique identifier of the user wallet.
* @return The primary cryptocurrency associated with the user wallet.
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If multi-currency user wallet
* @throws DataError.UserWalletError.WrongUserWallet If multi-currency user wallet
* ID provided.
*/
suspend fun getSingleCurrencyWalletWithCardCurrencies(userWalletId: UserWalletId): List<CryptoCurrency>
@ -87,7 +89,7 @@ interface CurrenciesRepository {
* @param userWalletId The unique identifier of the user wallet.
* @param id The unique identifier of the cryptocurrency to be retrieved.
* @return The cryptocurrency associated with the user wallet and ID.
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
suspend fun getSingleCurrencyWalletWithCardCurrency(
@ -102,11 +104,22 @@ interface CurrenciesRepository {
*
* @param userWalletId The unique identifier of the user wallet.
* @return A [Flow] emitting the set of cryptocurrencies associated with the user wallet.
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>>
/**
* Retrieves updates of the list of cryptocurrencies within a multi-currency wallet.
*
* Loads remote cryptocurrencies if they have expired.
*
* @param userWalletId The unique identifier of the user wallet.
* @return A [LceFlow] emitting the set of cryptocurrencies associated with the user wallet. May emit an
* [DataError.UserWalletError.WrongUserWallet] if single-currency user wallet ID provided.
*/
fun getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId: UserWalletId): LceFlow<Throwable, List<CryptoCurrency>>
/**
* Retrieves the list of cryptocurrencies within a multi-currency wallet.
*
@ -115,7 +128,7 @@ interface CurrenciesRepository {
* @param userWalletId The unique identifier of the user wallet.
* @param refresh A boolean flag indicating whether the data should be refreshed.
* @return A list of [CryptoCurrency].
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
suspend fun getMultiCurrencyWalletCurrenciesSync(
@ -129,7 +142,7 @@ interface CurrenciesRepository {
* @param userWalletId The unique identifier of the user wallet.
* @param id The unique identifier of the cryptocurrency to be retrieved.
* @return The cryptocurrency associated with the user wallet and ID.
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: CryptoCurrency.ID): CryptoCurrency
@ -152,7 +165,7 @@ interface CurrenciesRepository {
*
* @param userWalletId The unique identifier of the user wallet.
* @return A [Flow] emitting a boolean value indicating whether the tokens are grouped.
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean>
@ -162,7 +175,7 @@ interface CurrenciesRepository {
*
* @param userWalletId The unique identifier of the user wallet.
* @return A [Flow] emitting a boolean value indicating whether the tokens are sorted by balance.
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow<Boolean>

View file

@ -1,5 +1,6 @@
package com.tangem.domain.tokens.repository
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
@ -21,6 +22,20 @@ interface NetworksRepository {
*/
fun getNetworkStatusesUpdates(userWalletId: UserWalletId, networks: Set<Network>): Flow<Set<NetworkStatus>>
/**
* Retrieves updates of network statuses of specified blockchain networks for a specific user wallet.
*
* Loads remote network statuses if they have expired.
*
* @param userWalletId The unique identifier of the user wallet.
* @param networks A set of network which statuses are to be retrieved.
* @return A [LceFlow] emitting a set of [NetworkStatus] objects corresponding to the specified networks.
*/
fun getNetworkStatusesUpdatesLce(
userWalletId: UserWalletId,
networks: Set<Network>,
): LceFlow<Throwable, Set<NetworkStatus>>
/**
* Fetches pending transactions for given network
*

View file

@ -17,7 +17,6 @@ import com.tangem.domain.tokens.repository.MockCurrenciesRepository
import com.tangem.domain.tokens.repository.MockNetworksRepository
import com.tangem.domain.tokens.repository.MockQuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
@ -27,7 +26,6 @@ import org.junit.Test
internal class GetTokenListUseCaseTest {
private val dispatchers = TestingCoroutineDispatcherProvider()
private val userWalletId = UserWalletId(value = null)
@Ignore
@ -45,7 +43,7 @@ internal class GetTokenListUseCaseTest {
)
// When
val result = useCase(userWalletId)
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
@ -61,7 +59,7 @@ internal class GetTokenListUseCaseTest {
val useCase = getUseCase(tokens = flowOf(DataError.NetworkError.NoInternetConnection.left()))
// When
val result = useCase(userWalletId).first()
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
@ -81,7 +79,7 @@ internal class GetTokenListUseCaseTest {
)
// When
val result = useCase(userWalletId)
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
@ -97,7 +95,7 @@ internal class GetTokenListUseCaseTest {
val useCase = getUseCase(isGrouped = flowOf(DataError.NetworkError.NoInternetConnection.left()))
// When
val result = useCase(userWalletId).first()
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
@ -111,7 +109,7 @@ internal class GetTokenListUseCaseTest {
val useCase = getUseCase(isSortedByBalance = flowOf(DataError.NetworkError.NoInternetConnection.left()))
// When
val result = useCase(userWalletId).first()
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
@ -136,7 +134,7 @@ internal class GetTokenListUseCaseTest {
)
// When
val result = useCase(userWalletId)
val result = useCase.launch(userWalletId)
.take(count = 3)
.toList()
@ -155,7 +153,7 @@ internal class GetTokenListUseCaseTest {
val useCase = getUseCase(isGrouped = flowOf(true.right()))
// When
val result = useCase(userWalletId)
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
@ -177,7 +175,7 @@ internal class GetTokenListUseCaseTest {
)
// When
val result = useCase(userWalletId)
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
@ -199,7 +197,7 @@ internal class GetTokenListUseCaseTest {
)
// When
val result = useCase(userWalletId)
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
@ -214,7 +212,7 @@ internal class GetTokenListUseCaseTest {
val useCase = getUseCase(tokens = flowOf(emptyList<CryptoCurrency>().right()))
// When
val result = useCase(userWalletId).first()
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
@ -227,7 +225,7 @@ internal class GetTokenListUseCaseTest {
val useCase = getUseCase(tokens = flowOf())
// When
val result = useCase(userWalletId).first()
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
@ -243,7 +241,7 @@ internal class GetTokenListUseCaseTest {
val useCase = getUseCase(statuses = flowOf())
// When
val result = useCase(userWalletId)
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
@ -258,7 +256,7 @@ internal class GetTokenListUseCaseTest {
val useCase = getUseCase(statuses = flowOf(emptySet<NetworkStatus>().right()))
// When
val result = useCase(userWalletId).first()
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
@ -277,7 +275,7 @@ internal class GetTokenListUseCaseTest {
)
// When
val result = useCase(userWalletId)
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
@ -295,7 +293,7 @@ internal class GetTokenListUseCaseTest {
)
// When
val result = useCase(userWalletId).first()
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
@ -308,7 +306,6 @@ internal class GetTokenListUseCaseTest {
isGrouped: Flow<Either<DataError, Boolean>> = flowOf(MockTokenLists.isGrouped.right()),
isSortedByBalance: Flow<Either<DataError, Boolean>> = flowOf(MockTokenLists.isSortedByBalance.right()),
) = GetTokenListUseCase(
dispatchers = dispatchers,
currenciesRepository = MockCurrenciesRepository(
sortTokensResult = Unit.right(),
removeCurrencyResult = Unit.right(),

View file

@ -3,6 +3,8 @@ package com.tangem.domain.tokens.repository
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.domain.core.error.DataError
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.utils.toLce
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.FeePaidCurrency
@ -78,6 +80,12 @@ internal class MockCurrenciesRepository(
return tokens.map { it.getOrElse { e -> throw e } }
}
override fun getMultiCurrencyWalletCurrenciesUpdatesLce(
userWalletId: UserWalletId,
): LceFlow<Throwable, List<CryptoCurrency>> {
return tokens.map { it.toLce() }
}
override suspend fun getMultiCurrencyWalletCurrency(
userWalletId: UserWalletId,
id: CryptoCurrency.ID,

View file

@ -4,6 +4,8 @@ import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.domain.core.error.DataError
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.utils.toLce
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.wallets.models.UserWalletId
@ -22,6 +24,13 @@ internal class MockNetworksRepository(
return statuses.map { it.getOrElse { e -> throw e } }
}
override fun getNetworkStatusesUpdatesLce(
userWalletId: UserWalletId,
networks: Set<Network>,
): LceFlow<Throwable, Set<NetworkStatus>> {
return statuses.map { it.toLce() }
}
override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set<Network>) {
// no-op
}

View file

@ -24,6 +24,7 @@ dependencies {
implementation(projects.domain.models)
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.wallets.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)

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

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,4 @@
plugins {
alias(deps.plugins.kotlin.jvm)
id("configuration")
}

View file

@ -0,0 +1,20 @@
package com.tangem.domain.transaction.models
import java.math.BigDecimal
sealed class AssetRequirementsCondition {
/**
* The exact value of the fee for this type of condition is unknown.
*/
data object PaidTransaction : AssetRequirementsCondition()
/**
* The exact value of the fee for this type of condition is stored in `feeAmount`.
*/
data class PaidTransactionWithFee(
val feeAmount: BigDecimal,
val feeCurrencySymbol: String,
val decimals: Int,
) : AssetRequirementsCondition()
}

View file

@ -18,8 +18,22 @@ interface TransactionRepository {
destination: String,
userWalletId: UserWalletId,
network: Network,
isSwap: Boolean,
hash: String?,
): TransactionData?
@Suppress("LongParameterList")
suspend fun validateTransaction(
amount: Amount,
fee: Fee?,
memo: String?,
destination: String,
userWalletId: UserWalletId,
network: Network,
isSwap: Boolean = false,
hash: String? = null,
): Result<Unit>
suspend fun sendTransaction(
txData: TransactionData,
signer: CommonSigner,

View file

@ -0,0 +1,9 @@
package com.tangem.domain.transaction.error
import com.tangem.domain.tokens.model.CryptoCurrency
sealed class AssociateAssetError {
data class NotEnoughBalance(val feeCurrency: CryptoCurrency) : AssociateAssetError()
data class DataError(val message: String?) : AssociateAssetError()
}

View file

@ -0,0 +1,63 @@
package com.tangem.domain.transaction.usecase
import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.transaction.error.AssociateAssetError
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.isNullOrZero
class AssociateAssetUseCase(
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val walletManagersFacade: WalletManagersFacade,
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): Either<AssociateAssetError, Unit> {
return either {
val networkCoin = currenciesRepository.getNetworkCoin(
userWalletId = userWalletId,
networkId = currency.network.id,
derivationPath = currency.network.derivationPath,
)
if (isBalanceZero(userWalletId, networkCoin)) {
raise(AssociateAssetError.NotEnoughBalance(networkCoin))
}
val signer = cardSdkConfigRepository.getCommonSigner(cardId = null)
catch(
block = {
when (val result = walletManagersFacade.associateAsset(userWalletId, currency, signer)) {
is SimpleResult.Failure -> raise(AssociateAssetError.DataError(result.error.message))
SimpleResult.Success -> Unit
}
},
catch = { error -> AssociateAssetError.DataError(error.message) },
)
}
}
private suspend fun isBalanceZero(userWalletId: UserWalletId, currency: CryptoCurrency): Boolean {
val networkStatus = networksRepository.getNetworkStatusesSync(
userWalletId = userWalletId,
networks = setOf(currency.network),
).find { it.network == currency.network }
val networkCoinAmountStatus = (networkStatus?.value as? NetworkStatus.Verified)
?.amounts
?.get(currency.id)
return networkCoinAmountStatus is CryptoCurrencyAmountStatus.Loaded &&
networkCoinAmountStatus.value.isNullOrZero()
}
}

View file

@ -22,6 +22,8 @@ class CreateTransactionUseCase(
destination: String,
userWalletId: UserWalletId,
network: Network,
isSwap: Boolean = false,
hash: String? = null,
) = Either.catch {
requireNotNull(
transactionRepository.createTransaction(
@ -31,6 +33,8 @@ class CreateTransactionUseCase(
destination = destination,
userWalletId = userWalletId,
network = network,
isSwap = isSwap,
hash = hash,
),
) { "Failed to create transaction" }
}

View file

@ -0,0 +1,39 @@
package com.tangem.domain.transaction.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.wallets.models.UserWalletId
class ValidateTransactionUseCase(
private val transactionRepository: TransactionRepository,
) {
@Suppress("LongParameterList")
suspend operator fun invoke(
amount: Amount,
fee: Fee,
memo: String?,
destination: String,
userWalletId: UserWalletId,
network: Network,
isSwap: Boolean = false,
hash: String? = null,
): Either<Throwable, Unit> {
return transactionRepository.validateTransaction(
amount = amount,
fee = fee,
memo = memo,
destination = destination,
userWalletId = userWalletId,
network = network,
isSwap = isSwap,
hash = hash,
)
.fold(onSuccess = { Unit.right() }, onFailure = { it.left() })
}
}

View file

@ -17,6 +17,7 @@ dependencies {
// region Domain modules
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)

View file

@ -1,36 +1,22 @@
package com.tangem.domain.userwallets
package com.tangem.domain.wallets.builder
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
class UserWalletBuilder(
private val scanResponse: ScanResponse,
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
private val getCardImageUseCase: GetCardImageUseCase = GetCardImageUseCase(),
) {
private var backupCardsIds: Set<String> = emptySet()
private var hasBackupError: Boolean = false
private val CardDTO.isBackupNotAllowed: Boolean
get() = !this.settings.isBackupAllowed
private val ScanResponse.userWalletName: String
get() = when (productType) {
ProductType.Note -> "Note"
ProductType.Twins -> "Twin"
ProductType.Start2Coin -> "Start2Coin"
ProductType.Visa -> "Tangem Visa"
ProductType.Wallet,
ProductType.Wallet2,
ProductType.Ring,
-> when {
card.isBackupNotAllowed -> "Tangem card"
cardTypesResolver.isStart2Coin() -> "Start2Coin"
else -> "Wallet"
}
}
get() = !settings.isBackupAllowed
/**
* DANGEROUS!!!
@ -56,7 +42,11 @@ class UserWalletBuilder(
?.let {
UserWallet(
walletId = it,
name = userWalletName,
name = generateWalletNameUseCase(
productType = productType,
isBackupNotAllowed = card.isBackupNotAllowed,
isStartToCoin = cardTypesResolver.isStart2Coin(),
),
artworkUrl = getCardImageUseCase.invoke(card.cardId, card.cardPublicKey),
cardsInWallet = backupCardsIds.plus(card.cardId),
scanResponse = this,

View file

@ -1,4 +1,4 @@
package com.tangem.domain.userwallets
package com.tangem.domain.wallets.builder
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes

View file

@ -1,16 +0,0 @@
package com.tangem.domain.wallets.legacy
import arrow.core.raise.Raise
import arrow.core.raise.ensureNotNull
internal inline fun <Error> Raise<Error>.ensureUserWalletListManagerNotNull(
walletsStateHolder: WalletsStateHolder,
raise: (Throwable) -> Error,
): UserWalletsListManager {
return ensureNotNull(
value = walletsStateHolder.userWalletsListManager,
raise = {
raise(IllegalStateException("User wallets list manager not initialized"))
},
)
}

View file

@ -7,12 +7,20 @@ import kotlinx.coroutines.flow.Flow
interface UserWalletsListManager {
/**
* Indicates that the [UserWalletsListManager] is [UserWalletsListManager.Lockable]
* */
val isLockable: Boolean
/** [Flow] with all saved [UserWallet]s updates */
val userWallets: Flow<List<UserWallet>>
/** [Flow] with selected [UserWallet] updates */
val selectedUserWallet: Flow<UserWallet>
/** [List] with all saved [UserWallet]s updates */
val userWalletsSync: List<UserWallet>
/** Selected [UserWallet] */
val selectedUserWalletSync: UserWallet?
@ -84,11 +92,6 @@ interface UserWalletsListManager {
*/
suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet>
/**
* Indicates that the [UserWalletsListManager] supports [UserWalletsListManager.Lockable]
* */
fun isLockable(): Boolean
interface Lockable : UserWalletsListManager {
/**

View file

@ -49,7 +49,7 @@ suspend fun UserWalletsListManager.unlockIfLockable(type: UnlockType = UnlockTyp
* [UserWalletsListManager.Lockable] otherwise
* */
fun UserWalletsListManager.asLockable(): UserWalletsListManager.Lockable? {
if (this.isLockable()) {
if (this.isLockable) {
return this as? UserWalletsListManager.Lockable
}
return null

View file

@ -1,6 +0,0 @@
package com.tangem.domain.wallets.legacy
interface UserWalletsListManagerFeatureToggles {
val isGeneralManagerEnabled: Boolean
}

View file

@ -1,10 +0,0 @@
package com.tangem.domain.wallets.legacy
import kotlinx.coroutines.flow.Flow
interface WalletsStateHolder {
val userWalletsListManager: UserWalletsListManager?
val userWalletListManagerFlow: Flow<UserWalletsListManager?>
}

View file

@ -1,4 +1,4 @@
package com.tangem.domain.userwallets
package com.tangem.domain.wallets.models
data class Artwork(val artworkId: String) {
@ -6,9 +6,9 @@ data class Artwork(val artworkId: String) {
const val DEFAULT_IMG_URL = "https://app.tangem.com/cards/card_default.png"
const val SERGIO_CARD_URL = "https://app.tangem.com/cards/card_tg059.png"
const val MARTA_CARD_URL = "https://app.tangem.com/cards/card_tg083.png"
const val TWIN_CARD_1_URL = "https://app.tangem.com/cards/card_tg085.png"
const val TWIN_CARD_2_URL = "https://app.tangem.com/cards/card_tg086.png"
const val SERGIO_CARD_ID = "BC01"
const val MARTA_CARD_ID = "BC02"
const val TWIN_CARD_1 = "https://app.tangem.com/cards/card_tg085.png"
const val TWIN_CARD_2 = "https://app.tangem.com/cards/card_tg086.png"
}
}

View file

@ -2,7 +2,5 @@ package com.tangem.domain.wallets.models
sealed interface DeleteWalletError {
object DataError : DeleteWalletError
object UnableToDelete : DeleteWalletError
data object UnableToDelete : DeleteWalletError
}

View file

@ -2,7 +2,5 @@ package com.tangem.domain.wallets.models
sealed class GetUserWalletError {
data class DataError(val cause: Throwable) : GetUserWalletError()
object UserWalletNotFound : GetUserWalletError()
data object UserWalletNotFound : GetUserWalletError()
}

View file

@ -2,7 +2,5 @@ package com.tangem.domain.wallets.models
sealed interface SelectWalletError {
object DataError : SelectWalletError
object UnableToSelectUserWallet : SelectWalletError
}

View file

@ -2,5 +2,7 @@ package com.tangem.domain.wallets.models
sealed interface UpdateWalletError {
object DataError : UpdateWalletError
data object DataError : UpdateWalletError
data object NameAlreadyExists : UpdateWalletError
}

View file

@ -0,0 +1,11 @@
package com.tangem.domain.wallets.repository
/**
* Access to migrate names flag
*/
interface WalletNamesMigrationRepository {
suspend fun isMigrationDone(): Boolean
suspend fun setMigrationDone()
}

View file

@ -1,37 +1,36 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.raise.either
import arrow.core.right
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.DeleteWalletError
import com.tangem.domain.wallets.models.UserWalletId
/**
* Use case for updating user wallet
* Use case for deleting user wallet
*
* @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager'
* @property userWalletsListManager user wallets list manager
*
[REDACTED_AUTHOR]
*/
class DeleteWalletUseCase(private val walletsStateHolder: WalletsStateHolder) {
class DeleteWalletUseCase(private val userWalletsListManager: UserWalletsListManager) {
suspend operator fun invoke(userWalletId: UserWalletId): Either<DeleteWalletError, Unit> {
/**
* Deletes user wallet with provided ID.
*
* @param userWalletId ID of user wallet to be deleted.
*
* @return [Either] with [DeleteWalletError] or [Boolean] which indicates that there are still saved wallets.
* */
suspend operator fun invoke(userWalletId: UserWalletId): Either<DeleteWalletError, Boolean> {
return either {
val userWalletsListManager = ensureUserWalletListManagerNotNull(
walletsStateHolder = walletsStateHolder,
raise = { DeleteWalletError.DataError },
)
userWalletsListManager.delete(userWalletIds = listOf(userWalletId))
.doOnSuccess { return Unit.right() }
.doOnFailure { return DeleteWalletError.UnableToDelete.left() }
.doOnFailure {
raise(DeleteWalletError.UnableToDelete)
}
return Unit.right()
userWalletsListManager.hasUserWallets
}
}
}

View file

@ -0,0 +1,60 @@
package com.tangem.domain.wallets.usecase
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.wallets.legacy.UserWalletsListManager
/**
* Use case for user wallet name generation
*/
class GenerateWalletNameUseCase(
private val userWalletsListManager: UserWalletsListManager,
) {
operator fun invoke(productType: ProductType, isBackupNotAllowed: Boolean, isStartToCoin: Boolean): String {
val defaultName = getDefaultName(
productType = productType,
isBackupNotAllowed = isBackupNotAllowed,
isStartToCoin = isStartToCoin,
)
val existingNames = userWalletsListManager.userWalletsSync.map { it.name }.toSet()
return suggestedWalletName(defaultName, existingNames)
}
private fun suggestedWalletName(defaultName: String, existingNames: Set<String>): String {
val startIndex = 2
if (!existingNames.contains(defaultName)) {
return defaultName
}
for (index in startIndex..MAX_WALLETS_LIMIT) {
val potentialName = "$defaultName $index"
if (!existingNames.contains(potentialName)) {
return potentialName
}
}
return defaultName
}
private fun getDefaultName(productType: ProductType, isBackupNotAllowed: Boolean, isStartToCoin: Boolean): String {
return when (productType) {
ProductType.Note -> "Note"
ProductType.Twins -> "Twin"
ProductType.Start2Coin -> "Start2Coin"
ProductType.Visa -> "Tangem Visa"
ProductType.Wallet,
ProductType.Wallet2,
ProductType.Ring,
-> when {
isBackupNotAllowed -> "Tangem card"
isStartToCoin -> "Start2Coin"
else -> "Wallet"
}
}
}
companion object {
const val MAX_WALLETS_LIMIT = 10000
}
}

View file

@ -1,9 +1,10 @@
package com.tangem.domain.userwallets
package com.tangem.domain.wallets.usecase
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.common.TwinsHelper
import com.tangem.domain.wallets.models.Artwork
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.attestation.TangemApi
@ -42,8 +43,8 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier = OnlineCardV
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL
else -> when (TwinsHelper.getTwinCardNumber(cardId)) {
TwinCardNumber.First -> Artwork.TWIN_CARD_1
TwinCardNumber.Second -> Artwork.TWIN_CARD_2
TwinCardNumber.First -> Artwork.TWIN_CARD_1_URL
TwinCardNumber.Second -> Artwork.TWIN_CARD_2_URL
else -> Artwork.DEFAULT_IMG_URL
}
}

View file

@ -3,8 +3,7 @@ package com.tangem.domain.wallets.usecase
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensureNotNull
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.GetUserWalletError
import com.tangem.domain.wallets.models.UserWallet
@ -12,19 +11,14 @@ import com.tangem.domain.wallets.models.UserWallet
* Use case for getting selected wallet.
* Important! If all wallets is locked, use case returns a error.
*
* @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager'
* @property userWalletsListManager user wallets list manager
*
[REDACTED_AUTHOR]
*/
class GetSelectedWalletSyncUseCase(private val walletsStateHolder: WalletsStateHolder) {
class GetSelectedWalletSyncUseCase(private val userWalletsListManager: UserWalletsListManager) {
operator fun invoke(): Either<GetUserWalletError, UserWallet> {
return either {
val userWalletsListManager = ensureUserWalletListManagerNotNull(
walletsStateHolder = walletsStateHolder,
raise = GetUserWalletError::DataError,
)
ensureNotNull(
value = userWalletsListManager.selectedUserWalletSync,
raise = { GetUserWalletError.UserWalletNotFound },

View file

@ -2,8 +2,7 @@ package com.tangem.domain.wallets.usecase
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.GetUserWalletError
import com.tangem.domain.wallets.models.UserWallet
import kotlinx.coroutines.flow.Flow
@ -11,19 +10,14 @@ import kotlinx.coroutines.flow.Flow
/**
* Use case for getting flow of selected wallet.
*
* @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager'
* @property userWalletsListManager user wallets list manager
*
[REDACTED_AUTHOR]
*/
class GetSelectedWalletUseCase(private val walletsStateHolder: WalletsStateHolder) {
class GetSelectedWalletUseCase(private val userWalletsListManager: UserWalletsListManager) {
operator fun invoke(): Either<GetUserWalletError, Flow<UserWallet>> {
return either {
val userWalletsListManager = ensureUserWalletListManagerNotNull(
walletsStateHolder = walletsStateHolder,
raise = GetUserWalletError::DataError,
)
userWalletsListManager.selectedUserWallet
}
}

View file

@ -3,22 +3,15 @@ package com.tangem.domain.wallets.usecase
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensureNotNull
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.GetUserWalletError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.firstOrNull
class GetUserWalletUseCase(private val walletsStateHolder: WalletsStateHolder) {
class GetUserWalletUseCase(private val userWalletsListManager: UserWalletsListManager) {
suspend operator fun invoke(userWalletId: UserWalletId): Either<GetUserWalletError, UserWallet> = either {
val userWalletsListManager = ensureUserWalletListManagerNotNull(
walletsStateHolder = walletsStateHolder,
raise = GetUserWalletError::DataError,
)
val userWallets = userWalletsListManager.userWallets.firstOrNull().orEmpty()
operator fun invoke(userWalletId: UserWalletId): Either<GetUserWalletError, UserWallet> = either {
val userWallets = userWalletsListManager.userWalletsSync
ensureNotNull(userWallets.firstOrNull { it.walletId == userWalletId }) {
raise(GetUserWalletError.UserWalletNotFound)

View file

@ -0,0 +1,13 @@
package com.tangem.domain.wallets.usecase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
/**
* Use case for getting list of user wallets names.
*
* @property userWalletsListManager user wallets list manager
*/
class GetWalletNamesUseCase(private val userWalletsListManager: UserWalletsListManager) {
operator fun invoke(): List<String> = userWalletsListManager.userWalletsSync.map { it.name }
}

View file

@ -1,6 +1,6 @@
package com.tangem.domain.wallets.usecase
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
@ -8,16 +8,15 @@ import kotlinx.coroutines.flow.firstOrNull
/**
* Use case for getting list of user wallets
*
* @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager'
* @property userWalletsListManager user wallets list manager
*
[REDACTED_AUTHOR]
*/
class GetWalletsUseCase(private val walletsStateHolder: WalletsStateHolder) {
class GetWalletsUseCase(private val userWalletsListManager: UserWalletsListManager) {
@Throws(IllegalArgumentException::class)
operator fun invoke(): Flow<List<UserWallet>> =
requireNotNull(walletsStateHolder.userWalletsListManager).userWallets
operator fun invoke(): Flow<List<UserWallet>> = userWalletsListManager.userWallets
@Throws(IllegalArgumentException::class)
suspend fun invokeSync(): List<UserWallet>? = walletsStateHolder.userWalletsListManager?.userWallets?.firstOrNull()
suspend fun invokeSync(): List<UserWallet>? = userWalletsListManager.userWallets.firstOrNull()
}

View file

@ -1,19 +1,19 @@
package com.tangem.domain.wallets.usecase
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
/**
* Use case that checks if wallet need backup cards
*
* @property userWalletsListManager user wallets list manager
*/
class IsNeedToBackupUseCase(private val walletsStateHolder: WalletsStateHolder) {
class IsNeedToBackupUseCase(private val userWalletsListManager: UserWalletsListManager) {
operator fun invoke(id: UserWalletId): Flow<Boolean> {
val userWalletsListManager = requireNotNull(walletsStateHolder.userWalletsListManager)
return userWalletsListManager.userWallets
.map { wallets ->
val wallet = wallets.firstOrNull { it.walletId == id }

View file

@ -0,0 +1,36 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.raise.either
import arrow.core.right
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UpdateWalletError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
/**
* Use case for rename user wallet
*
* @property userWalletsListManager user wallets list manager
*/
class RenameWalletUseCase(private val userWalletsListManager: UserWalletsListManager) {
suspend operator fun invoke(userWalletId: UserWalletId, name: String): Either<UpdateWalletError, UserWallet> {
val existingNames = userWalletsListManager.userWalletsSync
if (existingNames.any { it.name == name && it.walletId != userWalletId }) {
return UpdateWalletError.NameAlreadyExists.left()
}
return either {
userWalletsListManager.update(userWalletId) { it.copy(name = name) }
.doOnSuccess { return it.right() }
.doOnFailure { return UpdateWalletError.DataError.left() }
return UpdateWalletError.DataError.left()
}
}
}

View file

@ -7,27 +7,21 @@ import arrow.core.right
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.SaveWalletError
import com.tangem.domain.wallets.models.UserWallet
/**
* Use case for saving user wallet
*
* @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager'
* @property userWalletsListManager user wallets list manager
*
[REDACTED_AUTHOR]
*/
class SaveWalletUseCase(private val walletsStateHolder: WalletsStateHolder) {
class SaveWalletUseCase(private val userWalletsListManager: UserWalletsListManager) {
suspend operator fun invoke(userWallet: UserWallet, canOverride: Boolean = false): Either<SaveWalletError, Unit> {
return either {
val userWalletsListManager = ensureUserWalletListManagerNotNull(
walletsStateHolder = walletsStateHolder,
raise = { SaveWalletError.DataError },
)
userWalletsListManager.save(userWallet, canOverride)
.doOnSuccess { return Unit.right() }
.doOnFailure {

View file

@ -5,8 +5,7 @@ import arrow.core.raise.either
import arrow.core.right
import com.tangem.common.CompletionResult
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.SelectWalletError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
@ -14,22 +13,18 @@ import com.tangem.domain.wallets.models.UserWalletId
/**
* Use case for selecting wallet
*
* @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager'
* @property userWalletsListManager user wallets list manager
* @property reduxStateHolder redux state holder
*
[REDACTED_AUTHOR]
*/
class SelectWalletUseCase(
private val walletsStateHolder: WalletsStateHolder,
private val userWalletsListManager: UserWalletsListManager,
private val reduxStateHolder: ReduxStateHolder,
) {
suspend operator fun invoke(userWalletId: UserWalletId): Either<SelectWalletError, UserWallet> {
return either {
val userWalletsListManager = ensureUserWalletListManagerNotNull(
walletsStateHolder = walletsStateHolder,
raise = { SelectWalletError.DataError },
)
return when (val result = userWalletsListManager.select(userWalletId)) {
is CompletionResult.Failure -> raise(SelectWalletError.UnableToSelectUserWallet)
is CompletionResult.Success -> {

View file

@ -5,23 +5,23 @@ import arrow.core.raise.either
import arrow.core.raise.ensureNotNull
import com.tangem.common.doOnFailure
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.wallets.models.UnlockWalletsError
/**
* Unlock wallets use case
*
* @property walletsStateHolder wallets state holder
* @property userWalletsListManager user wallets list manager
*
[REDACTED_AUTHOR]
*/
class UnlockWalletsUseCase(private val walletsStateHolder: WalletsStateHolder) {
class UnlockWalletsUseCase(private val userWalletsListManager: UserWalletsListManager) {
suspend operator fun invoke(type: UnlockType = UnlockType.ANY): Either<UnlockWalletsError, Unit> = either {
val userWalletsListManager = ensureNotNull(
value = walletsStateHolder.userWalletsListManager?.asLockable(),
value = userWalletsListManager.asLockable(),
raise = {
UnlockWalletsError.DataError(
cause = IllegalStateException("The lockable user wallets list manager could not be found"),

View file

@ -6,8 +6,7 @@ import arrow.core.raise.either
import arrow.core.right
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UpdateWalletError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
@ -15,22 +14,17 @@ import com.tangem.domain.wallets.models.UserWalletId
/**
* Use case for updating user wallet
*
* @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager'
* @property userWalletsListManager user wallets list manager
*
[REDACTED_AUTHOR]
*/
class UpdateWalletUseCase(private val walletsStateHolder: WalletsStateHolder) {
class UpdateWalletUseCase(private val userWalletsListManager: UserWalletsListManager) {
suspend operator fun invoke(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,
): Either<UpdateWalletError, UserWallet> {
return either {
val userWalletsListManager = ensureUserWalletListManagerNotNull(
walletsStateHolder = walletsStateHolder,
raise = { UpdateWalletError.DataError },
)
userWalletsListManager.update(userWalletId, update)
.doOnSuccess { return it.right() }
.doOnFailure { return UpdateWalletError.DataError.left() }