Updated on 2026-08-14
This commit is contained in:
commit
dbb63a396f
448 changed files with 12230 additions and 2075 deletions
1
domain/blockaid/.gitignore
vendored
Normal file
1
domain/blockaid/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
13
domain/blockaid/build.gradle.kts
Normal file
13
domain/blockaid/build.gradle.kts
Normal 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
1
domain/blockaid/models/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
10
domain/blockaid/models/build.gradle.kts
Normal file
10
domain/blockaid/models/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.domain.blockaid.models.transaction.simultation
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class AmountInfo(
|
||||
val amount: BigDecimal,
|
||||
val token: TokenInfo,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.domain.blockaid.models.transaction.simultation
|
||||
|
||||
data class TokenInfo(
|
||||
val chainId: Int?,
|
||||
val logoUrl: String?,
|
||||
val symbol: String,
|
||||
)
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -15,4 +15,5 @@ dependencies {
|
|||
implementation(projects.core.res)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.visa.models)
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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?)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.domain.models
|
||||
|
||||
data class ArtworkModel(
|
||||
val verifiedArtwork: ByteArray? = null,
|
||||
val defaultUrl: String,
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
1
domain/quotes/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
9
domain/quotes/build.gradle.kts
Normal file
9
domain/quotes/build.gradle.kts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.domain.core)
|
||||
api(projects.domain.tokens.models)
|
||||
}
|
||||
|
|
@ -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>?
|
||||
}
|
||||
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.quotes.multi
|
||||
|
||||
/**
|
||||
* Updater of quotes
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface MultiQuoteUpdater {
|
||||
|
||||
/** Subscribe */
|
||||
fun subscribe()
|
||||
|
||||
/** Unsubscribe */
|
||||
fun unsubscribe()
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>()
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>>()
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 */ },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,4 +8,8 @@ package com.tangem.domain.tokens
|
|||
interface TokensFeatureToggles {
|
||||
|
||||
val isNetworksLoadingRefactoringEnabled: Boolean
|
||||
|
||||
val isQuotesLoadingRefactoringEnabled: Boolean
|
||||
|
||||
val isStakingLoadingRefactoringEnabled: Boolean
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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" }
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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() })
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() })
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
package com.tangem.domain.walletconnect.model
|
||||
|
||||
interface WcMethod {
|
||||
sealed interface WcMethod {
|
||||
data object Unsupported : WcMethod
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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>,
|
||||
)
|
||||
}
|
||||
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -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<*>>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -25,6 +25,6 @@ class WcDisconnectUseCase(
|
|||
}
|
||||
|
||||
suspend fun disconnect(session: WcSession) {
|
||||
sessionsManager.removeSession(session.userWalletId, session)
|
||||
sessionsManager.removeSession(session)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue