Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-18 17:08:11 +03:00
commit dbb63a396f
448 changed files with 12230 additions and 2075 deletions

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

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,13 @@
plugins {
alias(deps.plugins.kotlin.jvm)
id("configuration")
}
dependencies {
/* Project - Domain */
implementation(projects.domain.core)
implementation(projects.domain.blockaid.models)
/* Other */
implementation(deps.moshi.adapters)
}

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

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,10 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.ksp)
id("configuration")
}
dependencies {
/* Other */
implementation(deps.moshi)
ksp(deps.moshi.kotlin.codegen)
}

View file

@ -0,0 +1,22 @@
package com.domain.blockaid.models.dapp
/**
* Result of BlockAid's DApp domain check
*/
enum class CheckDAppResult {
/**
* DApp was confirmed safe
*/
SAFE,
/**
* DApp was confirmed unsafe (known security risk)
*/
UNSAFE,
/**
* Check wasn't performed, BlockAid cannot guarantee DApp's safety
*/
FAILED_TO_VERIFY,
}

View file

@ -0,0 +1,11 @@
package com.domain.blockaid.models.dapp
/**
* Data BlockAid needs to verify DApp domain
*
* @property url DApp's domain url
*/
@JvmInline
value class DAppData(
val url: String,
)

View file

@ -0,0 +1,12 @@
package com.domain.blockaid.models.transaction
/**
* Result of BlockAid's transaction check
*
* @property validation Indicates whether the transaction is considered safe or unsafe
* @property simulation Provides insight into the expected outcome of the transaction
*/
data class CheckTransactionResult(
val validation: ValidationResult,
val simulation: SimulationResult,
)

View file

@ -0,0 +1,21 @@
package com.domain.blockaid.models.transaction
import com.domain.blockaid.models.transaction.simultation.SimulationData
/**
* Result of BlockAid's transaction simulation
*/
sealed class SimulationResult {
/**
* Simulation was successfully performed and returned data
*/
data class Success(
val data: SimulationData,
) : SimulationResult()
/**
* Simulation wasn't performed, BlockAid cannot guarantee transaction's behavior
*/
data object FailedToSimulate : SimulationResult()
}

View file

@ -0,0 +1,36 @@
package com.domain.blockaid.models.transaction
/**
* Input data for BlockAid's transaction check
*
* @property chain Chain name, for ex. "ethereum"
* @property accountAddress The address of the account (wallet) received the request in hex string format
* @property method Transaction method, for ex. "eth_signTransaction"
* @property domainUrl Url of the DApp domain
*/
data class TransactionData(
val chain: String,
val accountAddress: String,
val method: String,
val domainUrl: String,
val params: TransactionParams,
)
sealed class TransactionParams {
/**
* Parameters for Ethereum based transactions, can be taken from [WcSdkSessionRequest.JSONRPCRequest.params]
*/
data class Evm(
val params: String,
) : TransactionParams()
/**
* Parameters for Solana transactions
*
* @property transactions Base64-encoded serialized list of transactions
*/
data class Solana(
val transactions: List<String>,
) : TransactionParams()
}

View file

@ -0,0 +1,22 @@
package com.domain.blockaid.models.transaction
/**
* Result of BlockAid's transaction validation
*/
enum class ValidationResult {
/**
* Transaction was confirmed safe
*/
SAFE,
/**
* Transaction was confirmed unsafe
*/
UNSAFE,
/**
* Validation wasn't performed, BlockAid cannot guarantee transaction's safety
*/
FAILED_TO_VALIDATE,
}

View file

@ -0,0 +1,8 @@
package com.domain.blockaid.models.transaction.simultation
import java.math.BigDecimal
data class AmountInfo(
val amount: BigDecimal,
val token: TokenInfo,
)

View file

@ -0,0 +1,9 @@
package com.domain.blockaid.models.transaction.simultation
import java.math.BigDecimal
data class ApprovedAmount(
val approvedAmount: BigDecimal,
val isUnlimited: Boolean,
val tokenInfo: TokenInfo,
)

View file

@ -0,0 +1,22 @@
package com.domain.blockaid.models.transaction.simultation
/**
* Result of a successful transaction simulation.
*/
sealed class SimulationData {
/**
* Represents a swap/send/sell operations with specified send and receive amounts (can be multiple amounts for NFT)
*/
data class SendAndReceive(
val send: List<AmountInfo>,
val receive: List<AmountInfo>,
) : SimulationData()
/**
* Represents an approve operation with the specified amount (can be multiple amounts for NFT)
*/
data class Approve(
val approvedAmounts: List<ApprovedAmount>,
) : SimulationData()
}

View file

@ -0,0 +1,7 @@
package com.domain.blockaid.models.transaction.simultation
data class TokenInfo(
val chainId: Int?,
val logoUrl: String?,
val symbol: String,
)

View file

@ -0,0 +1,23 @@
package com.tangem.domain.blockaid
import arrow.core.Either
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.domain.blockaid.models.dapp.DAppData
import com.domain.blockaid.models.transaction.CheckTransactionResult
import com.domain.blockaid.models.transaction.TransactionData
/**
* Verifies the safety of DApps and WalletConnect transactions
*/
interface BlockAidVerifier {
/**
* Checks if a DApp is safe to use
*/
suspend fun verifyDApp(data: DAppData): Either<Throwable, CheckDAppResult>
/**
* Checks the safety of a WalletConnect transaction and provides a simulation result
*/
suspend fun verifyTransaction(data: TransactionData): Either<Throwable, CheckTransactionResult>
}

View file

@ -1,6 +1,7 @@
package com.tangem.domain.demo
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.smartcontract.SmartContractCallData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.common.transaction.TransactionSendResult
@ -24,8 +25,12 @@ class DemoTransactionSender(private val walletManager: WalletManager) : Transact
)
}
override suspend fun estimateFee(amount: Amount, destination: String): Result<TransactionFee> {
return getFee(amount, walletManager.wallet.address)
override suspend fun estimateFee(
amount: Amount,
destination: String,
callData: SmartContractCallData?,
): Result<TransactionFee> {
return getFee(amount, walletManager.wallet.address, callData)
}
override suspend fun send(

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

@ -10,6 +10,7 @@ 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.pagination.Page
import com.tangem.blockchain.common.smartcontract.SmartContractCallDataProviderFactory
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.common.trustlines.AssetRequirementsManager
@ -488,9 +489,20 @@ class DefaultWalletManagersFacade(
val destination = estimationFeeAddressFactory.makeAddress(blockchain)
val callData = if (amount.type is AmountType.Token) {
SmartContractCallDataProviderFactory.getTokenTransferCallData(
destinationAddress = destination,
amount = amount,
blockchain = blockchain,
)
} else {
null
}
(walletManager as? TransactionSender)?.estimateFee(
amount = amount,
destination = destination,
callData = callData,
)
}
@ -697,6 +709,11 @@ class DefaultWalletManagersFacade(
return walletManager.getSalePrice(collectionIdentifier, assetIdentifier)
}
override suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String? {
val blockchain = Blockchain.fromId(network.id.value)
return blockchain.getNFTExploreUrl(assetIdentifier)
}
override suspend fun isAccountInitialized(userWalletId: UserWalletId, network: Network): Boolean {
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network)
val initializableAccountWalletManger = walletManager as? InitializableAccount ?: return true

View file

@ -273,6 +273,8 @@ interface WalletManagersFacade {
assetIdentifier: NFTAsset.Identifier,
): NFTAsset.SalePrice?
suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String?
/**
* If wallet manager implements [InitializableAccount] then returns [InitializableAccount.isAccountInitialized]
* value. Otherwise always return true

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

@ -13,6 +13,7 @@ dependencies {
implementation(deps.arrow.core)
implementation(deps.kotlin.coroutines)
implementation(projects.core.analytics.models)
implementation(projects.core.utils)
implementation(projects.domain.core)

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()
}

View file

@ -0,0 +1,21 @@
package com.tangem.domain.nft
import arrow.core.raise.catch
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.tokens.model.Network
class GetNFTExploreUrlUseCase(
private val nftRepository: NFTRepository,
) {
suspend operator fun invoke(network: Network, assetIdentifier: NFTAsset.Identifier): String? = catch(
block = {
nftRepository.getNFTExploreUrl(
network = network,
assetIdentifier = assetIdentifier,
)
},
catch = { null },
)
}

View file

@ -0,0 +1,60 @@
package com.tangem.domain.nft.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
import com.tangem.core.analytics.models.AnalyticsParam.Key.COUNT
import com.tangem.core.analytics.models.AnalyticsParam.Key.STATE
sealed class NFTAnalyticsEvent(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(
category = "NFT",
event = event,
params = params,
) {
data class NFTListScreenOpened(
val state: State,
) : NFTAnalyticsEvent(
event = "NFT List Screen Opened",
params = buildMap {
put(STATE, state.value)
if (state is State.Full) {
put(COUNT, state.count.toString())
}
},
) {
sealed class State(val value: String) {
data object Empty : State("Empty")
data class Full(val count: Int) : State("Full")
}
}
object Receive {
data object ScreenOpened : NFTAnalyticsEvent(event = "Receive NFT Screen Opened")
data class BlockchainChosen(
private val blockchain: String,
) : NFTAnalyticsEvent(event = "Blockchain Chosen", params = mapOf(BLOCKCHAIN to blockchain))
data class CopyAddress(
private val blockchain: String,
) : NFTAnalyticsEvent(event = "Button - Copy Address", params = mapOf(BLOCKCHAIN to blockchain))
data class ShareAddress(
private val blockchain: String,
) : NFTAnalyticsEvent(event = "Button - Share Address", params = mapOf(BLOCKCHAIN to blockchain))
}
object Details {
data class ScreenOpened(
private val blockchain: String,
) : NFTAnalyticsEvent(event = "NFT Details Screen Opened", params = mapOf(BLOCKCHAIN to blockchain))
data object ButtonReadMore : NFTAnalyticsEvent(event = "Button - Read More")
data object ButtonSeeAll : NFTAnalyticsEvent(event = "Button - See All")
data object ButtonExplore : NFTAnalyticsEvent(event = "Button - Explore")
data object ButtonSend : NFTAnalyticsEvent(event = "Button - Send")
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.domain.nft.repository
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.domain.nft.models.NFTCollections
import com.tangem.domain.tokens.model.Network
@ -14,4 +15,6 @@ interface NFTRepository {
suspend fun refreshAssets(userWalletId: UserWalletId, network: Network, collectionId: NFTCollection.Identifier)
suspend fun isNFTSupported(network: Network): Boolean
suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String?
}

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

@ -9,21 +9,32 @@ sealed class YieldBalance {
abstract val integrationId: String?
abstract val address: String?
abstract val source: StatusSource
fun copySealed(source: StatusSource): YieldBalance {
return when (this) {
is Data -> copy(source = source)
is Empty -> copy(source = source)
is Error -> this
}
}
data class Data(
override val integrationId: String?,
override val address: String,
override val source: StatusSource,
val balance: YieldBalanceItem,
val source: StatusSource,
) : YieldBalance()
data class Empty(
override val integrationId: String?,
override val address: String,
val source: StatusSource,
override val source: StatusSource,
) : YieldBalance()
data class Error(override val integrationId: String?, override val address: String?) : YieldBalance()
data class Error(override val integrationId: String?, override val address: String?) : YieldBalance() {
override val source: StatusSource = StatusSource.ACTUAL
}
}
data class YieldBalanceItem(

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

@ -0,0 +1,17 @@
package com.tangem.domain.staking.multi
import com.tangem.domain.core.flow.FlowProducer
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.wallets.models.UserWalletId
/**
* Producer of all yield balances for selected wallet [UserWalletId]
*
[REDACTED_AUTHOR]
*/
interface MultiYieldBalanceProducer : FlowProducer<Set<YieldBalance>> {
data class Params(val userWalletId: UserWalletId)
interface Factory : FlowProducer.Factory<Params, MultiYieldBalanceProducer>
}

View file

@ -0,0 +1,18 @@
package com.tangem.domain.staking.multi
import com.tangem.domain.core.flow.FlowCachingSupplier
import com.tangem.domain.core.flow.FlowProducer
import com.tangem.domain.staking.model.stakekit.YieldBalance
/**
* Supplier of all yield balances for selected wallet [MultiYieldBalanceProducer.Params]
*
* @property factory factory for creating [MultiYieldBalanceProducer]
* @property keyCreator key creator
*
[REDACTED_AUTHOR]
*/
abstract class MultiYieldBalanceSupplier(
override val factory: FlowProducer.Factory<MultiYieldBalanceProducer.Params, MultiYieldBalanceProducer>,
override val keyCreator: (MultiYieldBalanceProducer.Params) -> String,
) : FlowCachingSupplier<MultiYieldBalanceProducer, MultiYieldBalanceProducer.Params, Set<YieldBalance>>()

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

@ -1,12 +1,12 @@
package com.tangem.domain.transaction
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.smartcontract.SmartContractCallData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionSendResult
import com.tangem.blockchain.common.transaction.TransactionsSendResult
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.transaction.models.TransactionType
import com.tangem.domain.wallets.models.UserWalletId
import java.math.BigDecimal
import java.math.BigInteger
@ -22,7 +22,16 @@ interface TransactionRepository {
userWalletId: UserWalletId,
network: Network,
txExtras: TransactionExtras?,
hash: String?,
): TransactionData.Uncompiled
@Suppress("LongParameterList")
suspend fun createTransferTransaction(
amount: Amount,
fee: Fee,
memo: String?,
destination: String,
userWalletId: UserWalletId,
network: Network,
): TransactionData.Uncompiled
@Suppress("LongParameterList")
@ -34,7 +43,6 @@ interface TransactionRepository {
spenderAddress: String,
userWalletId: UserWalletId,
network: Network,
hash: String?,
): TransactionData.Uncompiled
@Suppress("LongParameterList")
@ -45,9 +53,6 @@ interface TransactionRepository {
destination: String,
userWalletId: UserWalletId,
network: Network,
isSwap: Boolean = false,
txExtras: TransactionExtras?,
hash: String? = null,
): Result<Unit>
suspend fun sendTransaction(
@ -66,9 +71,8 @@ interface TransactionRepository {
): com.tangem.blockchain.extensions.Result<TransactionsSendResult>
fun createTransactionDataExtras(
data: String,
callData: SmartContractCallData,
network: Network,
transactionType: TransactionType,
nonce: BigInteger?,
gasLimit: BigInteger?,
): TransactionExtras
@ -78,4 +82,11 @@ interface TransactionRepository {
cryptoCurrency: CryptoCurrency.Token,
spenderAddress: String,
): BigDecimal
suspend fun prepareForSend(
transactionData: TransactionData,
signer: TransactionSigner,
userWalletId: UserWalletId,
network: Network,
): Result<ByteArray>
}

View file

@ -8,6 +8,9 @@ import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.domain.wallets.models.UserWalletId
import java.math.BigDecimal
/**
* Use case to create and get approval transaction
*/
class CreateApprovalTransactionUseCase(
private val transactionRepository: TransactionRepository,
) {
@ -20,7 +23,6 @@ class CreateApprovalTransactionUseCase(
fee: Fee,
contractAddress: String,
spenderAddress: String,
hash: String? = null,
) = Either.catch {
transactionRepository.createApprovalTransaction(
amount = BigDecimal.ZERO.convertToSdkAmount(cryptoCurrency),
@ -30,7 +32,6 @@ class CreateApprovalTransactionUseCase(
userWalletId = userWalletId,
network = cryptoCurrency.network,
fee = fee,
hash = hash,
)
}
}

View file

@ -1,30 +1,25 @@
package com.tangem.domain.transaction.usecase
import arrow.core.Either
import com.tangem.blockchain.common.smartcontract.CompiledSmartContractCallData
import com.tangem.common.extensions.hexToBytes
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.models.TransactionType
import java.math.BigInteger
class CreateTransactionDataExtrasUseCase(
private val transactionRepository: TransactionRepository,
) {
operator fun invoke(
data: String,
network: Network,
transactionType: TransactionType,
gasLimit: BigInteger? = null,
nonce: BigInteger? = null,
) = Either.catch {
requireNotNull(
transactionRepository.createTransactionDataExtras(
data = data,
network = network,
transactionType = transactionType,
nonce = nonce,
gasLimit = gasLimit,
),
) { "Failed to create transaction" }
}
operator fun invoke(data: String, network: Network, gasLimit: BigInteger? = null, nonce: BigInteger? = null) =
Either.catch {
requireNotNull(
transactionRepository.createTransactionDataExtras(
callData = CompiledSmartContractCallData(data.hexToBytes()),
network = network,
nonce = nonce,
gasLimit = gasLimit,
),
) { "Failed to create transaction" }
}
}

View file

@ -8,6 +8,12 @@ import com.tangem.domain.tokens.model.Network
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.wallets.models.UserWalletId
/**
* Use case to create and get transaction
*
* !!!IMPORTANT
* Use when transaction data is already compiled by external service or provider
*/
class CreateTransactionUseCase(
private val transactionRepository: TransactionRepository,
) {
@ -24,7 +30,6 @@ class CreateTransactionUseCase(
userWalletId: UserWalletId,
network: Network,
txExtras: TransactionExtras? = null,
hash: String? = null,
) = Either.catch {
transactionRepository.createTransaction(
amount = amount,
@ -34,7 +39,6 @@ class CreateTransactionUseCase(
userWalletId = userWalletId,
network = network,
txExtras = txExtras,
hash = hash,
)
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.domain.transaction.usecase
import arrow.core.Either
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
/**
* Use case to create and get transfer transaction
*
* !!!IMPORTANT
* Use when transaction data is compiled by us using BlockchainSDK methods
*/
class CreateTransferTransactionUseCase(
private val transactionRepository: TransactionRepository,
) {
/**
* [REDACTED_TODO_COMMENT]
*/
@Suppress("LongParameterList")
suspend operator fun invoke(
amount: Amount,
fee: Fee,
memo: String?,
destination: String,
userWalletId: UserWalletId,
network: Network,
) = Either.catch {
transactionRepository.createTransferTransaction(
amount = amount,
fee = fee,
memo = memo,
destination = destination,
userWalletId = userWalletId,
network = network,
)
}
}

View file

@ -17,6 +17,9 @@ import java.math.BigDecimal
/**
* Use case to get transaction fee
*
* !!!IMPORTANT!!!
* Use when transaction data is already compiled by external service or provider
*/
class GetFeeUseCase(
private val walletManagersFacade: WalletManagersFacade,

View file

@ -0,0 +1,102 @@
package com.tangem.domain.transaction.usecase
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.smartcontract.SmartContractCallDataProviderFactory
import com.tangem.blockchain.extensions.Result
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.demo.DemoTransactionSender
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.error.mapToFeeError
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import java.math.BigDecimal
/**
* Use case to get transfer transaction fee
*
* !!!IMPORTANT
* Use when transaction data is compiled by us using BlockchainSDK methods
*/
class GetTransferFeeUseCase(
private val walletManagersFacade: WalletManagersFacade,
private val demoConfig: DemoConfig,
) {
suspend operator fun invoke(
amount: BigDecimal,
destination: String,
userWallet: UserWallet,
cryptoCurrency: CryptoCurrency,
) = either {
catch(
block = {
val amountData = convertCryptoCurrencyToAmount(cryptoCurrency, amount)
val result = if (demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId)) {
demoTransactionSender(userWallet, cryptoCurrency).getFee(
amount = amountData,
destination = destination,
)
} else {
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWallet.walletId,
network = cryptoCurrency.network,
)
val smartContractCallData = if (amountData.type is AmountType.Token) {
SmartContractCallDataProviderFactory.getTokenTransferCallData(
amount = amountData,
destinationAddress = destination,
blockchain = Blockchain.fromId(cryptoCurrency.network.id.value),
)
} else {
null
}
(walletManager as? TransactionSender)?.getFee(
amount = amountData,
destination = destination,
callData = smartContractCallData,
) ?: error("Fee is null")
}
val maybeFee = when (result) {
is Result.Success -> result.data
is Result.Failure -> raise(result.mapToFeeError())
}
maybeFee
},
catch = {
raise(GetFeeError.DataError(it))
},
)
}
private suspend fun demoTransactionSender(
userWallet: UserWallet,
cryptoCurrency: CryptoCurrency,
): DemoTransactionSender {
return DemoTransactionSender(
walletManagersFacade
.getOrCreateWalletManager(userWallet.walletId, cryptoCurrency.network)
?: error("WalletManager is null"),
)
}
private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount(
currencySymbol = cryptoCurrency.symbol,
value = amount,
decimals = cryptoCurrency.decimals,
type = when (cryptoCurrency) {
is CryptoCurrency.Coin -> AmountType.Coin
is CryptoCurrency.Token -> AmountType.Token(
token = Token(
symbol = cryptoCurrency.symbol,
contractAddress = cryptoCurrency.contractAddress,
decimals = cryptoCurrency.decimals,
),
)
},
)
}

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

@ -21,8 +21,6 @@ class ValidateTransactionUseCase(
destination: String,
userWalletId: UserWalletId,
network: Network,
isSwap: Boolean = false,
hash: String? = null,
): Either<Throwable, Unit> {
return transactionRepository.validateTransaction(
amount = amount,
@ -31,10 +29,6 @@ class ValidateTransactionUseCase(
destination = destination,
userWalletId = userWalletId,
network = network,
isSwap = isSwap,
txExtras = null,
hash = hash,
)
.fold(onSuccess = { Unit.right() }, onFailure = { it.left() })
).fold(onSuccess = { Unit.right() }, onFailure = { it.left() })
}
}

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

@ -1,14 +1,23 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.domain.walletconnect"
}
dependencies {
/* Project - Domain */
implementation(projects.domain.core)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.walletConnect.models)
/* Other */
implementation(deps.moshi.adapters)
/* Tangem libraries */
implementation(tangemDeps.blockchain)
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.walletconnect.model
sealed interface WcEthMethod : WcMethod {
data class MessageSign(
val message: String,
val account: String,
) : WcEthMethod
data class SendTransaction(
val transaction: WcEthTransactionParams,
) : WcEthMethod
data class SignTransaction(
val transaction: WcEthTransactionParams,
) : WcEthMethod
}

View file

@ -0,0 +1,28 @@
package com.tangem.domain.walletconnect.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WcEthTransactionParams(
@Json(name = "from")
val from: String,
@Json(name = "to")
val to: String?,
@Json(name = "data")
val data: String,
@Json(name = "gas")
val gas: String?,
@Json(name = "gasPrice")
val gasPrice: String?,
@Json(name = "value")
val value: String?,
@Json(name = "nonce")
val nonce: String?,
)

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

@ -0,0 +1,14 @@
package com.tangem.domain.walletconnect.usecase.ethereum
import com.tangem.domain.walletconnect.usecase.sign.WcSignUseCase
interface WcEthMessageSignUseCase :
WcSignUseCase,
WcSignUseCase.SimpleRun<WcEthMessageSignUseCase.SignModel> {
data class SignModel(
val humanMsg: String,
val account: String,
val rawMsg: 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,24 @@
package com.tangem.domain.walletconnect.usecase.ethereum
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.domain.walletconnect.usecase.sign.WcSignUseCase
interface WcEthSendTransactionUseCase :
WcSignUseCase,
WcSignUseCase.SimpleRun<TransactionData> {
fun updateFee(fee: TransactionFee)
}
interface WcEthSignTransactionUseCase :
WcSignUseCase,
WcSignUseCase.SimpleRun<TransactionData> {
fun updateFee(fee: TransactionFee)
}
data class WcEthTransaction(
val isFeeByDap: Boolean,
val fee: TransactionFee,
)

View file

@ -1,23 +0,0 @@
package com.tangem.domain.walletconnect.usecase.pair
import arrow.core.Either
import com.tangem.domain.walletconnect.model.WcPairError
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.WcSessionApprove
import com.tangem.domain.walletconnect.model.WcSessionProposal
sealed interface WcPairState {
data object Loading : WcPairState
data class Error(val error: WcPairError) : WcPairState
data class Proposal(val dAppSession: WcSessionProposal) : WcPairState
sealed interface Approving : WcPairState {
val session: WcSessionApprove
data class Loading(override val session: WcSessionApprove) : Approving
data class Result(
override val session: WcSessionApprove,
val result: Either<WcPairError, WcSession>,
) : Approving
}
}

View file

@ -1,6 +1,10 @@
package com.tangem.domain.walletconnect.usecase.pair
import arrow.core.Either
import com.tangem.domain.walletconnect.model.WcPairError
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.WcSessionApprove
import com.tangem.domain.walletconnect.model.WcSessionProposal
import kotlinx.coroutines.flow.Flow
interface WcPairUseCase {
@ -11,4 +15,20 @@ interface WcPairUseCase {
fun reject()
enum class Source { QR, DEEPLINK, CLIPBOARD, ETC }
}
sealed interface WcPairState {
data object Loading : WcPairState
data class Error(val error: WcPairError) : WcPairState
data class Proposal(val dAppSession: WcSessionProposal) : WcPairState
sealed interface Approving : WcPairState {
val session: WcSessionApprove
data class Loading(override val session: WcSessionApprove) : Approving
data class Result(
override val session: WcSessionApprove,
val result: Either<WcPairError, WcSession>,
) : Approving
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.domain.walletconnect.usecase.sign
import arrow.core.Either
import com.tangem.domain.walletconnect.usecase.WcMethodUseCase
import kotlinx.coroutines.flow.Flow
interface WcSignUseCase : WcMethodUseCase {
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>>
}
}
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

@ -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")

Some files were not shown because too many files have changed in this diff Show more