Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-16 10:57:01 +03:00
commit 3c2a006fb9
331 changed files with 8261 additions and 1670 deletions

View file

@ -15,4 +15,5 @@ dependencies {
implementation(projects.core.res)
implementation(projects.domain.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.visa.models)
}

View file

@ -2,12 +2,43 @@ package com.tangem.domain.feedback
import com.tangem.domain.feedback.models.*
import com.tangem.domain.feedback.utils.breakLine
import com.tangem.domain.visa.model.VisaTxDetails
import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainAddresses
internal class FeedbackDataBuilder {
private val builder = StringBuilder()
fun addVisaTxInfo(txDetails: VisaTxDetails) {
builder.appendKeyValue("Type", txDetails.type)
builder.appendKeyValue("Status", txDetails.status)
builder.appendKeyValue("Blockchain amount", txDetails.blockchainAmount.toString())
builder.appendKeyValue("Transaction amount", txDetails.transactionAmount.toString())
builder.appendKeyValue("Currency code", txDetails.transactionCurrencyCode.toString())
builder.appendKeyValue("Merchant name", txDetails.merchantName)
builder.appendKeyValue("Merchant city", txDetails.merchantCity)
builder.appendKeyValue("Merchant country code", txDetails.merchantCountryCode)
builder.appendKeyValue("Merchant category code", txDetails.merchantCategoryCode)
builder.appendDelimiter()
builder.breakLine()
builder.append("Requests:")
txDetails.requests.forEach { request ->
builder.appendKeyValue("Type", request.requestType)
builder.appendKeyValue("Status", request.requestStatus)
builder.appendKeyValue("Blockchain amount", request.blockchainAmount.toString())
builder.appendKeyValue("Transaction amount", request.transactionAmount.toString())
builder.appendKeyValue("Currency code", request.txCurrencyCode.toString())
builder.appendKeyValue("Error code", request.errorCode.toString())
builder.appendKeyValue("Date", request.requestDate.toString())
builder.appendKeyValue("Transaction hash", request.txHash)
builder.appendKeyValue("Transaction status", request.txStatus)
builder.appendDelimiter()
builder.breakLine()
}
}
fun addUserWalletsInfo(userWalletsInfo: UserWalletsInfo) {
builder.appendKeyValue("User Wallet ID", userWalletsInfo.selectedUserWalletId)
builder.appendKeyValue("Total saved wallets", userWalletsInfo.totalUserWallets.toString())

View file

@ -2,7 +2,6 @@ package com.tangem.domain.feedback
import android.content.res.Resources
import com.tangem.core.res.getStringSafe
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
@ -27,7 +26,7 @@ class SendFeedbackEmailUseCase(
suspend operator fun invoke(type: FeedbackEmailType) {
val email = FeedbackEmail(
address = getAddress(type.cardInfo),
address = getAddress(type),
subject = emailSubjectResolver.resolve(type),
message = createMessage(type),
// Temporally user data is not sent
@ -37,8 +36,12 @@ class SendFeedbackEmailUseCase(
feedbackRepository.sendEmail(email)
}
private fun getAddress(cardInfo: CardInfo?): String {
return if (cardInfo?.isStart2Coin == true) START2COIN_SUPPORT_EMAIL else TANGEM_SUPPORT_EMAIL
private fun getAddress(type: FeedbackEmailType): String {
return when {
type is FeedbackEmailType.Visa || type.cardInfo?.isVisa == true -> TANGEM_VISA_SUPPORT_EMAIL
type.cardInfo?.isStart2Coin == true -> START2COIN_SUPPORT_EMAIL
else -> TANGEM_SUPPORT_EMAIL
}
}
private suspend fun createMessage(type: FeedbackEmailType): String {
@ -61,12 +64,15 @@ class SendFeedbackEmailUseCase(
is FeedbackEmailType.CurrencyDescriptionError,
is FeedbackEmailType.PreActivatedWallet,
is FeedbackEmailType.CardAttestationFailed,
is FeedbackEmailType.Visa.Dispute,
-> this
is FeedbackEmailType.DirectUserRequest,
is FeedbackEmailType.RateCanBeBetter,
is FeedbackEmailType.StakingProblem,
is FeedbackEmailType.SwapProblem,
is FeedbackEmailType.TransactionSendingProblem,
is FeedbackEmailType.Visa.Activation,
is FeedbackEmailType.Visa.DirectUserRequest,
-> {
append(resources.getStringSafe(R.string.feedback_data_collection_message))
skipLine()

View file

@ -11,6 +11,7 @@ data class CardInfo(
val signedHashesList: List<SignedHashes>,
val isImported: Boolean,
val isStart2Coin: Boolean,
val isVisa: Boolean,
) {
data class SignedHashes(val curve: String, val total: String?)

View file

@ -1,5 +1,7 @@
package com.tangem.domain.feedback.models
import com.tangem.domain.visa.model.VisaTxDetails
/**
* Email feedback type
*
@ -52,4 +54,15 @@ sealed interface FeedbackEmailType {
data object CardAttestationFailed : FeedbackEmailType {
override val cardInfo: CardInfo? = null
}
sealed class Visa : FeedbackEmailType {
data class DirectUserRequest(override val cardInfo: CardInfo) : Visa()
data class Activation(override val cardInfo: CardInfo) : Visa()
data class Dispute(
val visaTxDetails: VisaTxDetails,
override val cardInfo: CardInfo,
) : Visa()
}
}

View file

@ -4,6 +4,7 @@ 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
import com.tangem.domain.visa.model.VisaTxDetails
/**
* Email message body resolver
@ -29,11 +30,20 @@ internal class EmailMessageBodyResolver(
is FeedbackEmailType.ScanningProblem,
is FeedbackEmailType.CardAttestationFailed,
-> addPhoneInfoBody()
is FeedbackEmailType.Visa.Activation -> addUserRequestBody(type.cardInfo)
is FeedbackEmailType.Visa.DirectUserRequest -> addUserRequestBody(type.cardInfo)
is FeedbackEmailType.Visa.Dispute -> addVisaRequestBody(type.cardInfo, type.visaTxDetails)
}
return build()
}
private suspend fun FeedbackDataBuilder.addVisaRequestBody(cardInfo: CardInfo, visaTxDetails: VisaTxDetails) {
addUserRequestBody(cardInfo)
addDelimiter()
addVisaTxInfo(visaTxDetails)
}
private suspend fun FeedbackDataBuilder.addUserRequestBody(cardInfo: CardInfo) {
addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo(cardInfo.userWalletId))
addDelimiter()

View file

@ -20,6 +20,9 @@ internal class EmailMessageTitleResolver(private val resources: Resources) {
is FeedbackEmailType.DirectUserRequest,
is FeedbackEmailType.CurrencyDescriptionError,
is FeedbackEmailType.CardAttestationFailed,
is FeedbackEmailType.Visa.Activation,
is FeedbackEmailType.Visa.DirectUserRequest,
is FeedbackEmailType.Visa.Dispute,
-> R.string.feedback_preface_support
is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative
is FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed

View file

@ -37,6 +37,9 @@ internal class EmailSubjectResolver(private val resources: Resources) {
resources.getStringSafe(R.string.feedback_token_description_error)
}
FeedbackEmailType.CardAttestationFailed -> "Card attestation failed"
is FeedbackEmailType.Visa.Activation -> "[Visa] [Activation] {auto-filled subject}"
is FeedbackEmailType.Visa.DirectUserRequest -> "[Visa] {auto-filled subject}"
is FeedbackEmailType.Visa.Dispute -> "[Visa] [DISPUTE] {auto-filled subject}"
}
}
}

View file

@ -3,4 +3,5 @@ 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"
internal const val TANGEM_SUPPORT_EMAIL = "support@tangem.com"
internal const val TANGEM_VISA_SUPPORT_EMAIL = "pay@tangem.com"

View file

@ -14,6 +14,7 @@ dependencies {
api(projects.domain.core)
api(projects.domain.manageTokens.models)
api(projects.domain.networks)
api(projects.domain.quotes)
implementation(projects.domain.wallets.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.staking)

View file

@ -6,6 +6,7 @@ import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.managetokens.repository.CustomTokensRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.model.CryptoCurrency
@ -26,6 +27,7 @@ class SaveManagedTokensUseCase(
private val stakingRepository: StakingRepository,
private val quotesRepository: QuotesRepository,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val multiQuoteFetcher: MultiQuoteFetcher,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
@ -124,10 +126,19 @@ class SaveManagedTokensUseCase(
}
private suspend fun refreshUpdatedQuotes(addedCurrencies: List<CryptoCurrency>) {
quotesRepository.fetchQuotes(
currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId },
refresh = true,
)
if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) {
multiQuoteFetcher(
params = MultiQuoteFetcher.Params(
currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId },
appCurrencyId = null,
),
)
} else {
quotesRepository.fetchQuotes(
currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId },
refresh = true,
)
}
}
/**

View file

@ -20,6 +20,7 @@ dependencies {
api(projects.domain.models)
api(projects.domain.networks)
api(projects.domain.staking)
api(projects.domain.quotes)
api(projects.domain.wallets)
api(projects.domain.wallets.models)

View file

@ -3,6 +3,9 @@ package com.tangem.domain.markets
import arrow.core.None
import arrow.core.Option
import arrow.core.toOption
import com.tangem.domain.quotes.single.SingleQuoteProducer
import com.tangem.domain.quotes.single.SingleQuoteSupplier
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.repository.QuotesRepository
@ -11,9 +14,10 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@Suppress("UnusedPrivateMember")
class GetCurrencyQuotesUseCase(
private val quotesRepository: QuotesRepository,
private val singleQuoteSupplier: SingleQuoteSupplier,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
// TODO apply interval parameter [REDACTED_TASK_KEY]
operator fun invoke(
@ -23,9 +27,18 @@ class GetCurrencyQuotesUseCase(
): Flow<Option<Quote.Value>> {
val rawId = currencyID.rawCurrencyId ?: return flowOf(None)
return quotesRepository.getQuotesUpdatesLegacy(
currenciesIds = setOf(rawId),
refresh = refresh,
).map { it.filterIsInstance<Quote.Value>().firstOrNull().toOption() }.catch { emit(None) }
return if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) {
singleQuoteSupplier(
params = SingleQuoteProducer.Params(rawCurrencyId = rawId),
)
.map { (it as? Quote.Value).toOption() }
} else {
quotesRepository.getQuotesUpdatesLegacy(
currenciesIds = setOf(rawId),
refresh = refresh,
)
.map { it.filterIsInstance<Quote.Value>().firstOrNull().toOption() }
}
.catch { emit(None) }
}
}

View file

@ -4,6 +4,7 @@ import arrow.core.Either
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.model.CryptoCurrency
@ -31,6 +32,7 @@ class SaveMarketTokensUseCase(
private val stakingRepository: StakingRepository,
private val quotesRepository: QuotesRepository,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val multiQuoteFetcher: MultiQuoteFetcher,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
@ -106,9 +108,18 @@ class SaveMarketTokensUseCase(
}
private suspend fun refreshUpdatedQuotes(addedCurrencies: List<CryptoCurrency>) {
quotesRepository.fetchQuotes(
currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId },
refresh = true,
)
if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) {
multiQuoteFetcher(
params = MultiQuoteFetcher.Params(
currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId },
appCurrencyId = null,
),
)
} else {
quotesRepository.fetchQuotes(
currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId },
refresh = true,
)
}
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.domain.models
data class ArtworkModel(
val verifiedArtwork: ByteArray? = null,
val defaultUrl: String,
)

View file

@ -55,6 +55,14 @@ data class NFTAsset(
override val stringValue: String = tokenAddress
}
@Serializable
data class Solana(
val tokenAddress: String,
val cnft: Boolean,
) : Identifier() {
override val stringValue: String = tokenAddress
}
@Serializable
data object Unknown : Identifier() {
override val stringValue: String = ""

View file

@ -40,6 +40,9 @@ data class NFTCollection(
@Serializable
data class TON(val contractAddress: String?) : Identifier()
@Serializable
data class Solana(val collection: String?) : Identifier()
@Serializable
data object Unknown : Identifier()
}

1
domain/quotes/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,9 @@
plugins {
alias(deps.plugins.kotlin.jvm)
id("configuration")
}
dependencies {
api(projects.domain.core)
api(projects.domain.tokens.models)
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.quotes
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
/**
* Quotes repository
*
[REDACTED_AUTHOR]
*/
interface QuotesRepositoryV2 {
/** Get quotes by [currenciesIds] synchronously or null */
suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set<CryptoCurrency.RawID>): Set<Quote>?
}

View file

@ -0,0 +1,23 @@
package com.tangem.domain.quotes.multi
import com.tangem.domain.core.flow.FlowFetcher
import com.tangem.domain.tokens.model.CryptoCurrency
/**
* Fetcher of quotes
*
[REDACTED_AUTHOR]
*/
interface MultiQuoteFetcher : FlowFetcher<MultiQuoteFetcher.Params> {
/**
* Params
*
* @property currenciesIds identifiers of currencies
* @property appCurrencyId app currency id, if null then selected app currency will be used
*/
data class Params(
val currenciesIds: Set<CryptoCurrency.RawID>,
val appCurrencyId: String?,
)
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.quotes.multi
/**
* Updater of quotes
*
[REDACTED_AUTHOR]
*/
interface MultiQuoteUpdater {
/** Subscribe */
fun subscribe()
/** Unsubscribe */
fun unsubscribe()
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.quotes.single
import com.tangem.domain.core.flow.FlowProducer
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
/**
* Producer of quote [CryptoCurrency.RawID]
*
[REDACTED_AUTHOR]
*/
interface SingleQuoteProducer : FlowProducer<Quote> {
data class Params(val rawCurrencyId: CryptoCurrency.RawID)
interface Factory : FlowProducer.Factory<Params, SingleQuoteProducer>
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.quotes.single
import com.tangem.domain.core.flow.FlowCachingSupplier
import com.tangem.domain.tokens.model.Quote
/**
* Supplier of quote [SingleQuoteProducer.Params]
*
* @property factory factory for creating [SingleQuoteProducer]
* @property keyCreator key creator
*
[REDACTED_AUTHOR]
*/
abstract class SingleQuoteSupplier(
override val factory: SingleQuoteProducer.Factory,
override val keyCreator: (SingleQuoteProducer.Params) -> String,
) : FlowCachingSupplier<SingleQuoteProducer, SingleQuoteProducer.Params, Quote>()

View file

@ -1,10 +1,7 @@
package com.tangem.domain.staking
import arrow.core.Either
import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.staking.model.stakekit.BalanceType
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.Token
import com.tangem.domain.staking.model.stakekit.*
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.domain.staking.repositories.StakingErrorResolver
@ -51,6 +48,9 @@ class InvalidatePendingTransactionsUseCase(
}
StakingActionType.WITHDRAW -> {
modifyBalancesByStatus(balances, action, BalanceType.UNSTAKED)
if (token.network == NetworkType.TON) {
modifyBalancesByStatus(balances, action, BalanceType.PREPARING)
}
}
StakingActionType.UNLOCK_LOCKED -> {
modifyBalancesByStatus(balances, action, BalanceType.LOCKED)

View file

@ -28,6 +28,7 @@ dependencies {
implementation(projects.domain.promo.models)
implementation(projects.domain.promo)
implementation(projects.domain.networks)
implementation(projects.domain.quotes)
/** Project - Api */
implementation(projects.features.send.api)

View file

@ -7,6 +7,13 @@ sealed interface Quote {
val rawCurrencyId: CryptoCurrency.RawID
fun copySealed(source: StatusSource): Quote {
return when (this) {
is Empty -> this
is Value -> copy(source = source)
}
}
/**
* Represents unknown financial information for a specific cryptocurrency.
*

View file

@ -5,6 +5,7 @@ import arrow.core.raise.Raise
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
@ -12,6 +13,9 @@ 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.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
/**
* A use case for adding multiple cryptocurrencies to a user's wallet.
@ -20,12 +24,14 @@ import com.tangem.domain.wallets.models.UserWalletId
* network statuses, particularly after the addition of new tokens.
*/
// TODO: Add tests
@Suppress("LongParameterList")
class AddCryptoCurrenciesUseCase(
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
private val quotesRepository: QuotesRepository,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val multiQuoteFetcher: MultiQuoteFetcher,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
@ -68,9 +74,14 @@ class AddCryptoCurrenciesUseCase(
val currencyToAdd = currency.takeUnless(existingCurrencies::contains) ?: return@either
addCurrencies(userWalletId, currencyToAdd)
refreshUpdatedNetworks(userWalletId, currencyToAdd, existingCurrencies)
refreshUpdatedYieldBalances(userWalletId, currencyToAdd)
refreshUpdatedQuotes(currencyToAdd)
coroutineScope {
awaitAll(
async { refreshUpdatedNetworks(userWalletId, currencyToAdd, existingCurrencies) },
async { refreshUpdatedYieldBalances(userWalletId, currencyToAdd) },
async { refreshUpdatedQuotes(currencyToAdd) },
)
}
}
suspend operator fun invoke(
@ -94,9 +105,15 @@ class AddCryptoCurrenciesUseCase(
}
val tokenToAdd = createTokenCurrency(userWalletId, contractAddress, networkId)
addCurrencies(userWalletId, tokenToAdd)
refreshUpdatedNetworks(userWalletId, tokenToAdd, existingCurrencies)
refreshUpdatedYieldBalances(userWalletId, tokenToAdd)
refreshUpdatedQuotes(tokenToAdd)
coroutineScope {
awaitAll(
async { refreshUpdatedNetworks(userWalletId, tokenToAdd, existingCurrencies) },
async { refreshUpdatedYieldBalances(userWalletId, tokenToAdd) },
async { refreshUpdatedQuotes(tokenToAdd) },
)
}
tokenToAdd
}
@ -150,10 +167,19 @@ class AddCryptoCurrenciesUseCase(
}
private suspend fun refreshUpdatedQuotes(currencyToAdd: CryptoCurrency) {
quotesRepository.fetchQuotes(
currenciesIds = setOfNotNull(currencyToAdd.id.rawCurrencyId),
refresh = true,
)
if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) {
multiQuoteFetcher(
params = MultiQuoteFetcher.Params(
currenciesIds = setOfNotNull(currencyToAdd.id.rawCurrencyId),
appCurrencyId = null,
),
)
} else {
quotesRepository.fetchQuotes(
currenciesIds = setOfNotNull(currencyToAdd.id.rawCurrencyId),
refresh = true,
)
}
}
private suspend fun Raise<Throwable>.createTokenCurrency(

View file

@ -5,6 +5,7 @@ import arrow.core.raise.Raise
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
@ -17,12 +18,14 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
@Suppress("LongParameterList")
class FetchCardTokenListUseCase(
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
private val quotesRepository: QuotesRepository,
private val stakingRepository: StakingRepository,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val multiQuoteFetcher: MultiQuoteFetcher,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
@ -90,10 +93,16 @@ class FetchCardTokenListUseCase(
}
private suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.RawID>, refresh: Boolean) {
catch(
block = { quotesRepository.getQuotesSync(currenciesIds, refresh) },
catch = { /* Ignore error */ },
)
if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) {
multiQuoteFetcher(
params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null),
)
} else {
catch(
block = { quotesRepository.getQuotesSync(currenciesIds, refresh) },
catch = { /* Ignore error */ },
)
}
}
private suspend fun fetchYieldBalances(

View file

@ -5,6 +5,7 @@ import arrow.core.raise.Raise
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrency
@ -27,12 +28,14 @@ import kotlinx.coroutines.coroutineScope
* @param quotesRepository The repository for retrieving cryptocurrency quotes.
*/
// TODO: Add tests
@Suppress("LongParameterList")
class FetchCurrencyStatusUseCase(
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
private val quotesRepository: QuotesRepository,
private val stakingRepository: StakingRepository,
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
private val multiQuoteFetcher: MultiQuoteFetcher,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
@ -134,10 +137,19 @@ class FetchCurrencyStatusUseCase(
}
private suspend fun Raise<CurrencyStatusError>.fetchQuote(currencyId: CryptoCurrency.ID, refresh: Boolean) {
catch(
block = { quotesRepository.getQuotesSync(setOfNotNull(currencyId.rawCurrencyId), refresh) },
) {
raise(CurrencyStatusError.DataError(it))
if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) {
multiQuoteFetcher(
params = MultiQuoteFetcher.Params(
currenciesIds = setOfNotNull(currencyId.rawCurrencyId),
appCurrencyId = null,
),
)
} else {
catch(
block = { quotesRepository.getQuotesSync(setOfNotNull(currencyId.rawCurrencyId), refresh) },
) {
raise(CurrencyStatusError.DataError(it))
}
}
}

View file

@ -7,6 +7,7 @@ import arrow.core.raise.either
import arrow.core.raise.ensureNotNull
import arrow.core.toNonEmptyListOrNull
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
@ -29,12 +30,14 @@ import kotlinx.coroutines.coroutineScope
* @param stakingRepository The repository for retrieving staking-related data.
*/
// TODO: Add tests
@Suppress("LongParameterList")
class FetchTokenListUseCase(
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
private val quotesRepository: QuotesRepository,
private val stakingRepository: StakingRepository,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val multiQuoteFetcher: MultiQuoteFetcher,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
@ -125,13 +128,22 @@ class FetchTokenListUseCase(
}
private suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean) {
catch(
block = {
val rawIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet()
quotesRepository.getQuotesSync(rawIds, refresh)
},
) {
/* Ignore error */
if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) {
multiQuoteFetcher(
params = MultiQuoteFetcher.Params(
currenciesIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet(),
appCurrencyId = null,
),
)
} else {
catch(
block = {
val rawIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet()
quotesRepository.getQuotesSync(rawIds, refresh)
},
) {
/* Ignore error */
}
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.models.UserWalletId
class GetCryptoCurrenciesUseCase(
private val currenciesRepository: CurrenciesRepository,
) {
/**
* Retrieves the list of cryptocurrencies within a multi-currency wallet.
*
* @param userWalletId The unique identifier of the user wallet.
*
* @return An [Either] representing success (Right) or an error (Left) in fetching the status.
*/
suspend operator fun invoke(userWalletId: UserWalletId): Either<CurrencyStatusError, List<CryptoCurrency>> {
return Either.catch {
currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId)
}.mapLeft(CurrencyStatusError::DataError)
}
}

View file

@ -4,6 +4,7 @@ import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.tokens.error.QuotesError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -16,6 +17,8 @@ import kotlinx.coroutines.coroutineScope
class RefreshMultiCurrencyWalletQuotesUseCase(
private val quotesRepository: QuotesRepository,
private val currenciesRepository: CurrenciesRepository,
private val multiQuoteFetcher: MultiQuoteFetcher,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
suspend operator fun invoke(userWalletId: UserWalletId): Either<QuotesError, Unit> {
@ -45,14 +48,23 @@ class RefreshMultiCurrencyWalletQuotesUseCase(
}
private suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.ID>) {
catch(
block = {
quotesRepository.fetchQuotes(
if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) {
multiQuoteFetcher(
params = MultiQuoteFetcher.Params(
currenciesIds = currenciesIds.mapNotNullTo(hashSetOf(), CryptoCurrency.ID::rawCurrencyId),
refresh = true,
)
},
catch = { /* Ignore error */ },
)
appCurrencyId = null,
),
)
} else {
catch(
block = {
quotesRepository.fetchQuotes(
currenciesIds = currenciesIds.mapNotNullTo(hashSetOf(), CryptoCurrency.ID::rawCurrencyId),
refresh = true,
)
},
catch = { /* Ignore error */ },
)
}
}
}

View file

@ -8,4 +8,8 @@ package com.tangem.domain.tokens
interface TokensFeatureToggles {
val isNetworksLoadingRefactoringEnabled: Boolean
val isQuotesLoadingRefactoringEnabled: Boolean
val isStakingLoadingRefactoringEnabled: Boolean
}

View file

@ -10,6 +10,9 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusProducer
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import com.tangem.domain.networks.single.SingleNetworkStatusProducer
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.quotes.QuotesRepositoryV2
import com.tangem.domain.quotes.single.SingleQuoteProducer
import com.tangem.domain.quotes.single.SingleQuoteSupplier
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.YieldBalanceList
import com.tangem.domain.staking.repositories.StakingRepository
@ -37,10 +40,12 @@ import kotlinx.coroutines.flow.*
abstract class BaseCurrencyStatusOperations(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
private val quotesRepositoryV2: QuotesRepositoryV2,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
private val singleQuoteSupplier: SingleQuoteSupplier,
private val tokensFeatureToggles: TokensFeatureToggles,
) {
@ -171,7 +176,16 @@ abstract class BaseCurrencyStatusOperations(
} else {
currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, cryptoCurrencyId)
}
val quote = cryptoCurrencyId.rawCurrencyId?.let { quotesRepository.getQuoteSync(it) }?.right()
val quote = cryptoCurrencyId.rawCurrencyId?.let { rawId ->
if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) {
singleQuoteSupplier(params = SingleQuoteProducer.Params(rawCurrencyId = rawId))
.firstOrNull()
} else {
quotesRepository.getQuoteSync(rawId)
}
}
?.right()
?: Error.EmptyQuotes.left()
val networkStatuses = if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
@ -237,7 +251,12 @@ abstract class BaseCurrencyStatusOperations(
?: return emptyList<CryptoCurrencyStatus>().right()
val (networks, currenciesIds) = getIds(nonEmptyCurrencies)
val rawIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet()
val quotes = quotesRepository.getQuotesSync(rawIds, false).right()
val quotes = if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) {
quotesRepositoryV2.getMultiQuoteSyncOrNull(currenciesIds = rawIds)?.right()
} else {
quotesRepository.getQuotesSync(rawIds, false).right()
}
val networkStatuses = if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
multiNetworkStatusSupplier(
@ -268,13 +287,23 @@ abstract class BaseCurrencyStatusOperations(
block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) },
catch = { raise(Error.DataError(it)) },
)
val quotes = catch(
block = {
currency.id.rawCurrencyId?.let { quotesRepository.getQuoteSync(it) }
?.right() ?: Error.EmptyQuotes.left()
},
catch = { Error.DataError(it).left() },
)
val quotes = if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) {
currency.id.rawCurrencyId?.let {
singleQuoteSupplier(params = SingleQuoteProducer.Params(rawCurrencyId = it))
.firstOrNull()
}
?.right()
?: Error.EmptyQuotes.left()
} else {
catch(
block = {
currency.id.rawCurrencyId?.let { quotesRepository.getQuoteSync(it) }
?.right() ?: Error.EmptyQuotes.left()
},
catch = { Error.DataError(it).left() },
)
}
val networkStatus = if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) {
singleNetworkStatusSupplier(

View file

@ -14,6 +14,10 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import com.tangem.domain.networks.single.SingleNetworkStatusProducer
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.quotes.QuotesRepositoryV2
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.quotes.single.SingleQuoteProducer
import com.tangem.domain.quotes.single.SingleQuoteSupplier
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.YieldBalanceList
import com.tangem.domain.staking.repositories.StakingRepository
@ -34,20 +38,25 @@ import kotlinx.coroutines.flow.*
class CachedCurrenciesStatusesOperations(
private val currenciesRepository: CurrenciesRepository,
private val quotesRepository: QuotesRepository,
quotesRepositoryV2: QuotesRepositoryV2,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val multiQuoteFetcher: MultiQuoteFetcher,
private val singleQuoteSupplier: SingleQuoteSupplier,
private val tokensFeatureToggles: TokensFeatureToggles,
) : BaseCurrenciesStatusesOperations,
BaseCurrencyStatusOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
quotesRepositoryV2 = quotesRepositoryV2,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
singleQuoteSupplier = singleQuoteSupplier,
tokensFeatureToggles = tokensFeatureToggles,
) {
@ -178,7 +187,14 @@ class CachedCurrenciesStatusesOperations(
},
async {
val rawCurrenciesIds = currenciesIds.mapNotNullTo(mutableSetOf()) { it.rawCurrencyId }
quotesRepository.fetchQuotes(rawCurrenciesIds)
if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) {
multiQuoteFetcher(
params = MultiQuoteFetcher.Params(currenciesIds = rawCurrenciesIds, appCurrencyId = null),
)
} else {
quotesRepository.fetchQuotes(rawCurrenciesIds)
}
},
async { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies) },
)
@ -249,27 +265,44 @@ class CachedCurrenciesStatusesOperations(
}
private fun getQuotes(tokensIds: NonEmptySet<CryptoCurrency.ID>): Flow<Either<TokenListError, Set<Quote>>> {
return quotesRepository.getQuotesUpdates(tokensIds.mapNotNull { it.rawCurrencyId }.toSet())
.map<Set<Quote>, Either<TokenListError, Set<Quote>>> { it.right() }
.retryWhen { cause, _ ->
emit(TokenListError.DataError(cause).left())
// adding delay before retry to avoid spam when flow restarted
delay(RETRY_DELAY)
true
}
.distinctUntilChanged()
return if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) {
getQuotesUpdates(
rawCurrencyIds = tokensIds.mapNotNullTo(
destination = hashSetOf(),
transform = CryptoCurrency.ID::rawCurrencyId,
),
)
} else {
quotesRepository.getQuotesUpdates(tokensIds.mapNotNull { it.rawCurrencyId }.toSet())
.map<Set<Quote>, Either<TokenListError, Set<Quote>>> { it.right() }
.retryWhen { cause, _ ->
emit(TokenListError.DataError(cause).left())
// adding delay before retry to avoid spam when flow restarted
delay(RETRY_DELAY)
true
}
.distinctUntilChanged()
}
}
override fun getQuotes(id: CryptoCurrency.RawID): Flow<Either<Error, Set<Quote>>> {
return quotesRepository.getQuotesUpdates(setOf(id))
.map<Set<Quote>, Either<Error, Set<Quote>>> { it.right() }
.retryWhen { cause, _ ->
emit(Error.DataError(cause).left())
// adding delay before retry to avoid spam when flow restarted
delay(RETRY_DELAY)
true
}
.distinctUntilChanged()
return if (tokensFeatureToggles.isQuotesLoadingRefactoringEnabled) {
singleQuoteSupplier(
params = SingleQuoteProducer.Params(rawCurrencyId = id),
)
.map<Quote, Either<Error, Set<Quote>>> { setOf(it).right() }
.distinctUntilChanged()
} else {
quotesRepository.getQuotesUpdates(setOf(id))
.map<Set<Quote>, Either<Error, Set<Quote>>> { it.right() }
.retryWhen { cause, _ ->
emit(Error.DataError(cause).left())
// adding delay before retry to avoid spam when flow restarted
delay(RETRY_DELAY)
true
}
.distinctUntilChanged()
}
}
override fun getNetworksStatuses(
@ -363,6 +396,33 @@ class CachedCurrenciesStatusesOperations(
.distinctUntilChanged()
}
// temporary code because token list is built using networks list
private fun getQuotesUpdates(rawCurrencyIds: Set<CryptoCurrency.RawID>): EitherFlow<TokenListError, Set<Quote>> {
return channelFlow {
val state = MutableStateFlow(emptySet<Quote>())
rawCurrencyIds.onEach {
launch {
singleQuoteSupplier(
params = SingleQuoteProducer.Params(rawCurrencyId = it),
)
.onEach { quote ->
state.update { loadedStatuses ->
loadedStatuses.addOrReplace(quote) { it.rawCurrencyId == quote.rawCurrencyId }
}
}
.launchIn(scope = this)
}
}
state
.onEach(::send)
.launchIn(scope = this)
}
.map<Set<Quote>, Either<TokenListError, Set<Quote>>> { it.right() }
.distinctUntilChanged()
}
private fun isFetchingStarted(userWalletId: UserWalletId): Boolean {
return fetchingState.value[userWalletId]?.let { it.isStarted() || it.isFinished() } ?: false
}

View file

@ -180,10 +180,15 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest {
tokensFeatureToggles = object : TokensFeatureToggles {
override val isNetworksLoadingRefactoringEnabled: Boolean = false
override val isQuotesLoadingRefactoringEnabled: Boolean = false
override val isStakingLoadingRefactoringEnabled: Boolean = false
},
singleNetworkStatusSupplier = mockk(),
multiNetworkStatusFetcher = mockk(),
multiNetworkStatusSupplier = mockk(),
multiQuoteFetcher = mockk(),
singleQuoteSupplier = mockk(),
quotesRepositoryV2 = mockk(),
),
dispatchers = dispatchers,
)

View file

@ -78,4 +78,11 @@ interface TransactionRepository {
cryptoCurrency: CryptoCurrency.Token,
spenderAddress: String,
): BigDecimal
suspend fun prepareForSend(
transactionData: TransactionData,
signer: TransactionSigner,
userWalletId: UserWalletId,
network: Network,
): Result<ByteArray>
}

View file

@ -0,0 +1,38 @@
package com.tangem.domain.transaction.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.blockchain.common.TransactionData
import com.tangem.domain.card.models.TwinKey
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.wallets.models.UserWallet
class PrepareForSendUseCase(
private val transactionRepository: TransactionRepository,
private val cardSdkConfigRepository: CardSdkConfigRepository,
) {
suspend operator fun invoke(
transactionData: TransactionData,
userWallet: UserWallet,
network: Network,
): Either<Throwable, ByteArray> {
val card = userWallet.scanResponse.card
val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins
val signer = cardSdkConfigRepository.getCommonSigner(
cardId = card.cardId.takeIf { isCardNotBackedUp },
twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse),
)
return transactionRepository.prepareForSend(
transactionData = transactionData,
userWalletId = userWallet.walletId,
network = network,
signer = signer,
)
.fold(onSuccess = { it.right() }, onFailure = { it.left() })
}
}

View file

@ -0,0 +1,40 @@
package com.tangem.domain.transaction.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.domain.card.models.TwinKey
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
class SignUseCase(
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val walletManagersFacade: WalletManagersFacade,
) {
suspend operator fun invoke(
hash: ByteArray,
userWallet: UserWallet,
network: Network,
): Either<TangemError, ByteArray> {
val card = userWallet.scanResponse.card
val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins
val signer = cardSdkConfigRepository.getCommonSigner(
cardId = card.cardId.takeIf { isCardNotBackedUp },
twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse),
)
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWallet.walletId, network)
?: error("WalletManager not found")
return when (val signResult = signer.sign(hash, walletManager.wallet.publicKey)) {
is CompletionResult.Failure -> signResult.error.left()
is CompletionResult.Success -> signResult.data.right()
}
}
}

View file

@ -10,5 +10,6 @@ dependencies {
ksp(deps.moshi.kotlin.codegen)
implementation(deps.moshi.adapters)
implementation(deps.kotlin.serialization)
implementation(deps.jodatime)
implementation(projects.core.error)
}

View file

@ -8,6 +8,6 @@ import kotlinx.serialization.Serializable
@JsonClass(generateAdapter = true)
data class VisaActivationOrderInfo(
@Json(name = "orderId") val orderId: String,
@Json(name = "customer_id") val customerId: String,
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
@Json(name = "customerId") val customerId: String,
@Json(name = "customerWalletAddress") val customerWalletAddress: String,
)

View file

@ -20,9 +20,14 @@ sealed class VisaActivationRemoteState {
data object PaymentAccountDeploying : VisaActivationRemoteState()
@Serializable
data class WaitingPinCode(
data class AwaitingPinCode(
val activationOrderInfo: VisaActivationOrderInfo,
) : VisaActivationRemoteState()
val status: Status,
) : VisaActivationRemoteState() {
enum class Status {
WaitingForPinCode, InProgress, WasError
}
}
@Serializable
data object WaitingForActivationFinishing : VisaActivationRemoteState()
@ -43,6 +48,7 @@ class VisaActivationRemoteState_Json(
@Json(name = "type") val type: VisaActivationRemoteState_Type,
@Json(name = "requestCardWallet") val requestCardWallet: VisaCardWalletDataToSignRequest? = null,
@Json(name = "activationOrderInfo") val activationOrderInfo: VisaActivationOrderInfo? = null,
@Json(name = "awaiting_pin_code_status") val awaitingPinCodeStatus: AwaitingPinCodeStatus_Type? = null,
)
@Suppress("ClassNaming")
@ -58,6 +64,12 @@ enum class VisaActivationRemoteState_Type {
BlockedForActivation,
}
@Suppress("ClassNaming")
@JsonClass(generateAdapter = false)
enum class AwaitingPinCodeStatus_Type {
WaitingForPinCode, InProgress, WasError
}
@Suppress("ClassNaming")
class VisaActivationRemoteState_JsonAdapter {
@ -70,7 +82,17 @@ class VisaActivationRemoteState_JsonAdapter {
VisaActivationRemoteState.CustomerWalletSignatureRequired(value.activationOrderInfo!!)
VisaActivationRemoteState_Type.PaymentAccountDeploying -> VisaActivationRemoteState.PaymentAccountDeploying
VisaActivationRemoteState_Type.WaitingPinCode ->
VisaActivationRemoteState.WaitingPinCode(value.activationOrderInfo!!)
VisaActivationRemoteState.AwaitingPinCode(
activationOrderInfo = value.activationOrderInfo!!,
status = when (value.awaitingPinCodeStatus!!) {
AwaitingPinCodeStatus_Type.WaitingForPinCode ->
VisaActivationRemoteState.AwaitingPinCode.Status.WaitingForPinCode
AwaitingPinCodeStatus_Type.InProgress ->
VisaActivationRemoteState.AwaitingPinCode.Status.InProgress
AwaitingPinCodeStatus_Type.WasError ->
VisaActivationRemoteState.AwaitingPinCode.Status.WasError
},
)
VisaActivationRemoteState_Type.WaitingForActivationFinishing ->
VisaActivationRemoteState.WaitingForActivationFinishing
VisaActivationRemoteState_Type.Activated -> VisaActivationRemoteState.Activated
@ -93,10 +115,18 @@ class VisaActivationRemoteState_JsonAdapter {
)
is VisaActivationRemoteState.PaymentAccountDeploying ->
VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.PaymentAccountDeploying)
is VisaActivationRemoteState.WaitingPinCode ->
is VisaActivationRemoteState.AwaitingPinCode ->
VisaActivationRemoteState_Json(
VisaActivationRemoteState_Type.WaitingPinCode,
activationOrderInfo = value.activationOrderInfo,
awaitingPinCodeStatus = when (value.status) {
VisaActivationRemoteState.AwaitingPinCode.Status.WaitingForPinCode ->
AwaitingPinCodeStatus_Type.WaitingForPinCode
VisaActivationRemoteState.AwaitingPinCode.Status.InProgress ->
AwaitingPinCodeStatus_Type.InProgress
VisaActivationRemoteState.AwaitingPinCode.Status.WasError ->
AwaitingPinCodeStatus_Type.WasError
},
)
is VisaActivationRemoteState.WaitingForActivationFinishing ->
VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.WaitingForActivationFinishing)

View file

@ -6,6 +6,7 @@ plugins {
dependencies {
/* Project - Domain */
implementation(projects.domain.core)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.walletConnect.models)

View file

@ -0,0 +1,9 @@
package com.tangem.domain.walletconnect.model
sealed interface WcEthMethod : WcMethod {
data class PersonalEthSign(
val message: String,
val account: String,
) : WcEthMethod
}

View file

@ -1,5 +1,5 @@
package com.tangem.domain.walletconnect.model
interface WcMethod {
sealed interface WcMethod {
data object Unsupported : WcMethod
}

View file

@ -1,14 +0,0 @@
package com.tangem.domain.walletconnect.model
import com.tangem.domain.tokens.model.Network
sealed interface WcNetwork {
val name: String
data class Supported(val network: Network) : WcNetwork {
override val name: String get() = network.name
}
data class Unknown(override val name: String) : WcNetwork
}

View file

@ -4,7 +4,7 @@ sealed class WcPairError(override val message: String) : Exception(message) {
data object UnsupportedDApp : WcPairError("UnsupportedDApp")
data class UnsupportedNetworks(
val chains: Set<WcNetwork.Unknown>,
val chains: Set<String>,
) : WcPairError("ApprovalErrorMissingNetworks")
data class ExternalApprovalError(

View file

@ -1,9 +1,9 @@
package com.tangem.domain.walletconnect.model
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.UserWallet
data class WcSession(
val userWalletId: UserWalletId,
val wallet: UserWallet,
val sdkModel: WcSdkSession,
)

View file

@ -1,8 +1,9 @@
package com.tangem.domain.walletconnect.model
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
data class WcSessionApprove(
val walletId: UserWalletId,
val network: List<WcNetwork.Supported>,
val wallet: UserWallet,
val network: List<Network>,
)

View file

@ -1,19 +1,20 @@
package com.tangem.domain.walletconnect.model
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.UserWallet
data class WcSessionProposal(
val dAppMetaData: WcAppMetaData,
val proposalNetwork: Map<UserWalletId, ProposalNetwork>,
val proposalNetwork: Map<UserWallet, ProposalNetwork>,
val securityStatus: Any,
) {
data class ProposalNetwork(
val walletId: UserWalletId,
val missingRequired: Set<WcNetwork.Supported>,
val required: Set<WcNetwork.Supported>,
val available: Set<WcNetwork.Supported>,
val notAdded: Set<WcNetwork.Supported>,
val wallet: UserWallet,
val missingRequired: Set<Network>,
val required: Set<Network>,
val available: Set<Network>,
val notAdded: Set<Network>,
)
}

View file

@ -2,12 +2,12 @@ package com.tangem.domain.walletconnect.repository
import arrow.core.Either
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.UserWallet
import kotlinx.coroutines.flow.Flow
interface WcSessionsManager {
val sessions: Flow<Map<UserWalletId, List<WcSession>>>
suspend fun saveSession(userWalletId: UserWalletId, session: WcSession)
suspend fun removeSession(userWalletId: UserWalletId, session: WcSession): Either<Throwable, Unit>
val sessions: Flow<Map<UserWallet, List<WcSession>>>
suspend fun saveSession(session: WcSession)
suspend fun removeSession(session: WcSession): Either<Throwable, Unit>
suspend fun findSessionByTopic(topic: String): WcSession?
}

View file

@ -1,8 +0,0 @@
package com.tangem.domain.walletconnect.request
import com.tangem.domain.walletconnect.model.WcRequest
import kotlinx.coroutines.flow.Flow
interface WcRequestService {
val requests: Flow<WcRequest<*>>
}

View file

@ -1,9 +0,0 @@
package com.tangem.domain.walletconnect.respond
import arrow.core.Either
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
interface WcRespondService {
suspend fun respond(request: WcSdkSessionRequest, response: String): Either<Throwable, Unit>
suspend fun rejectRequest(request: WcSdkSessionRequest, message: String = ""): Either<Throwable, Unit>
}

View file

@ -2,16 +2,16 @@ package com.tangem.domain.walletconnect.usecase
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.UserWallet
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
class WcSessionsUseCase(private val sessionsManager: WcSessionsManager) {
operator fun invoke(): Flow<Map<UserWalletId, List<WcSession>>> {
operator fun invoke(): Flow<Map<UserWallet, List<WcSession>>> {
return sessionsManager.sessions
}
suspend fun invokeSync(): Map<UserWalletId, List<WcSession>> {
suspend fun invokeSync(): Map<UserWallet, List<WcSession>> {
return sessionsManager.sessions.first()
}
}

View file

@ -1,25 +0,0 @@
package com.tangem.domain.walletconnect.usecase
import arrow.core.Either
import kotlinx.coroutines.flow.Flow
interface WcSimpleSignUseCase<SignModel, MiddleAction> : WcUseCase {
fun signFlow(initModel: SignModel): Flow<State<SignModel>>
fun action(action: MiddleAction)
fun cancel()
fun sign(toSign: SignModel)
sealed interface State<SignModel> {
val model: SignModel
data class PreSign<SignModel>(override val model: SignModel) : State<SignModel>
data class Signing<SignModel>(override val model: SignModel) : State<SignModel>
data class Result<SignModel>(
val result: Either<Throwable, Unit>,
override val model: SignModel,
) : State<SignModel>
}
}

View file

@ -1,3 +1,13 @@
package com.tangem.domain.walletconnect.usecase
interface WcUseCase
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
interface WcUseCase
interface WcMethodUseCase : WcUseCase {
val session: WcSession
val rawSdkRequest: WcSdkSessionRequest
val network: Network
}

View file

@ -25,6 +25,6 @@ class WcDisconnectUseCase(
}
suspend fun disconnect(session: WcSession) {
sessionsManager.removeSession(session.userWalletId, session)
sessionsManager.removeSession(session)
}
}

View file

@ -1,72 +0,0 @@
package com.tangem.domain.walletconnect.usecase.ethereum
import arrow.core.Either
import com.tangem.domain.walletconnect.model.WcRequest
import com.tangem.domain.walletconnect.respond.WcRespondService
import com.tangem.domain.walletconnect.usecase.WcUseCase
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
class EthPersonalSignUseCase(
private val wcRequest: WcRequest<WcEthMethod.SignMessage>,
private val respondService: WcRespondService,
) : WcUseCase {
private val onCallTerminalAction = Channel<TerminalAction>()
fun signFlow(): Flow<State> = flow {
val model = SignModel(wcRequest.method.raw)
emit(State.PreSign(model))
when (val action = onCallTerminalAction.receiveAsFlow().first()) {
TerminalAction.Cancel -> {
val result = respondService.rejectRequest(wcRequest.rawSdkRequest)
emit(State.Result(result, model))
}
is TerminalAction.Sign -> {
emit(State.Signing(action.toSign))
val signed = signAndPrepareForSend()
val result =
if (signed != null) {
respondService.respond(wcRequest.rawSdkRequest, signed)
} else {
respondService.rejectRequest(wcRequest.rawSdkRequest)
}
emit(State.Result(result, model))
}
}
}
@Suppress("FunctionOnlyReturningConstant") // todo(wc) remove later
private suspend fun signAndPrepareForSend(): String? {
return null // todo(wc)
}
fun sign(toSign: SignModel) {
onCallTerminalAction.trySend(TerminalAction.Sign(toSign))
}
fun cancel() {
onCallTerminalAction.trySend(TerminalAction.Cancel)
}
sealed interface TerminalAction {
data class Sign(val toSign: SignModel) : TerminalAction
data object Cancel : TerminalAction
}
sealed interface State {
val model: SignModel
data class PreSign(override val model: SignModel) : State
data class Signing(override val model: SignModel) : State
data class Result(
val result: Either<Throwable, Unit>,
override val model: SignModel,
) : State
}
data class SignModel(
val raw: List<String>,
)
}

View file

@ -1,10 +0,0 @@
package com.tangem.domain.walletconnect.usecase.ethereum
import com.tangem.domain.walletconnect.model.WcMethod
sealed interface WcEthMethod : WcMethod {
data class SignMessage(
val raw: List<String>,
) : WcEthMethod
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.walletconnect.usecase.ethereum
import com.tangem.domain.walletconnect.usecase.sign.WcSignUseCase
interface WcPersonalEthSignUseCase :
WcSignUseCase,
WcSignUseCase.FinalAction,
WcSignUseCase.SimpleRun<WcPersonalEthSignUseCase.SignModel> {
data class SignModel(
val humanMsg: String,
val account: String,
val rawMsg: String,
)
}

View file

@ -0,0 +1,14 @@
package com.tangem.domain.walletconnect.usecase.sign
import arrow.core.Either
data class WcSignState<SignModel>(
val signModel: SignModel,
val domainStep: WcSignStep,
)
sealed interface WcSignStep {
data object PreSign : WcSignStep
data object Signing : WcSignStep
data class Result(val result: Either<Throwable, Unit>) : WcSignStep
}

View file

@ -0,0 +1,20 @@
package com.tangem.domain.walletconnect.usecase.sign
import com.tangem.domain.walletconnect.usecase.WcMethodUseCase
import kotlinx.coroutines.flow.Flow
interface WcSignUseCase : WcMethodUseCase {
interface FinalAction {
fun cancel()
fun sign()
}
interface SimpleRun<SignModel> {
operator fun invoke(): Flow<WcSignState<SignModel>>
}
interface ArgsRun<SignModel, Args> {
operator fun invoke(args: Args): Flow<WcSignState<SignModel>>
}
}

View file

@ -10,7 +10,6 @@ import com.tangem.domain.models.scan.ScanResponse
*
* @property name User wallet name
* @property walletId User wallet [UserWalletId]
* @property artworkUrl User wallet card artwork URL
* @property cardsInWallet List of cards IDs assigned with this user's wallet. The list will be empty if the wallet
* has been backed up on another device.
* @property isMultiCurrency Indicates whether this user wallet can work with more than one currency
@ -22,8 +21,6 @@ data class UserWallet(
val name: String,
@Json(name = "walletId")
val walletId: UserWalletId,
@Json(name = "artworkUrl")
val artworkUrl: String,
@Json(name = "cardsInWallet")
val cardsInWallet: Set<String>,
@Json(name = "isMultiCurrency")

View file

@ -5,7 +5,6 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -13,7 +12,6 @@ import dagger.assisted.AssistedInject
class UserWalletBuilder @AssistedInject constructor(
@Assisted private val scanResponse: ScanResponse,
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
private val getCardImageUseCase: GetCardImageUseCase,
) {
private var backupCardsIds: Set<String> = emptySet()
private var hasBackupError: Boolean = false
@ -38,7 +36,7 @@ class UserWalletBuilder @AssistedInject constructor(
this.hasBackupError = hasBackupError
}
suspend fun build(): UserWallet? {
fun build(): UserWallet? {
return with(scanResponse) {
UserWalletIdBuilder.scanResponse(scanResponse)
.build()
@ -50,7 +48,6 @@ class UserWalletBuilder @AssistedInject constructor(
isBackupNotAllowed = card.isBackupNotAllowed,
isStartToCoin = cardTypesResolver.isStart2Coin(),
),
artworkUrl = getCardImageUseCase.invoke(card.cardId, card.cardPublicKey),
cardsInWallet = backupCardsIds.plus(card.cardId),
scanResponse = this,
isMultiCurrency = cardTypesResolver.isMultiwalletAllowed(),

View file

@ -4,8 +4,12 @@ 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.models.ArtworkModel
import com.tangem.domain.wallets.models.Artwork
import com.tangem.operations.attestation.ArtworkSize
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.sdk.api.featuretoggles.CardSdkFeatureToggles
/**
* Use case for getting card image url
@ -14,7 +18,11 @@ import com.tangem.operations.attestation.OnlineCardVerifier
*
[REDACTED_AUTHOR]
*/
class GetCardImageUseCase(private val verifier: OnlineCardVerifier) {
class GetCardImageUseCase(
private val verifier: OnlineCardVerifier,
private val cardArtworksProvider: CardArtworksProvider,
private val cardSdkFeatureToggles: CardSdkFeatureToggles,
) {
/**
* Get card image url
@ -22,14 +30,30 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier) {
* @param cardId card id
* @param cardPublicKey card public key
*/
suspend operator fun invoke(cardId: String, cardPublicKey: ByteArray): String {
suspend operator fun invoke(cardId: String, cardPublicKey: ByteArray, size: ArtworkSize): ArtworkModel {
return if (cardSdkFeatureToggles.isNewArtworkLoadingEnabled) {
val result = cardArtworksProvider.getArtwork(
cardId = cardId,
cardPublicKey = cardPublicKey,
size = size,
)
when (result) {
is Result.Failure -> ArtworkModel(null, getFallbackArtworkUrl(cardId))
is Result.Success -> ArtworkModel(result.data, getFallbackArtworkUrl(cardId))
}
} else {
ArtworkModel(null, getLegacyArtwork(cardId, cardPublicKey))
}
}
private suspend fun getLegacyArtwork(cardId: String, cardPublicKey: ByteArray): String {
return when (val result = verifier.getCardInfo(cardId, cardPublicKey)) {
is Result.Success -> {
val artworkId = result.data.artwork?.id
if (artworkId.isNullOrEmpty()) {
getFallbackArtworkUrl(cardId)
} else {
OnlineCardVerifier.getUrlForArtwork(
CardArtworksProvider.getUrlForArtwork(
cardId = cardId,
cardPublicKey = cardPublicKey.toHexString(),
artworkId = artworkId,
@ -41,8 +65,6 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier) {
}
}
fun getDefaultFallbackUrl(): String = Artwork.DEFAULT_IMG_URL
private fun getFallbackArtworkUrl(cardId: String): String {
return when {
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
@ -50,7 +72,7 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier) {
else -> when (TwinsHelper.getTwinCardNumber(cardId)) {
TwinCardNumber.First -> Artwork.TWIN_CARD_1_URL
TwinCardNumber.Second -> Artwork.TWIN_CARD_2_URL
else -> getDefaultFallbackUrl()
else -> Artwork.DEFAULT_IMG_URL
}
}
}