Updated on 2026-08-14

This commit is contained in:
Tangem 2023-08-22 19:05:00 +03:00
parent f2abb0ee1c
commit 731fd53e3d
15 changed files with 369 additions and 49 deletions

View file

@ -1,10 +1,14 @@
package com.tangem.data.tokens.utils
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.model.PendingTransaction
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import timber.log.Timber
import java.math.BigDecimal
internal class NetworkStatusFactory {
@ -19,10 +23,18 @@ internal class NetworkStatusFactory {
value = when (result) {
is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation
is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable
is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount(result.amountToCreateAccount)
is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount(
address = getNetworkAddress(result.defaultAddress, result.addresses),
amountToCreateAccount = result.amountToCreateAccount,
)
is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified(
amounts = formatAmounts(result.tokensAmounts, currencies),
hasTransactionsInProgress = result.hasTransactionsInProgress,
address = getNetworkAddress(result.defaultAddress, result.addresses),
amounts = formatAmounts(result.currenciesAmounts, currencies),
pendingTransactions = formatTransactions(
networksAddresses = result.addresses,
transactions = result.currentTransactions,
currencies = currencies,
),
)
},
)
@ -39,13 +51,91 @@ internal class NetworkStatusFactory {
is CryptoCurrencyAmount.Coin -> currencies.singleOrNull { it is CryptoCurrency.Coin }
is CryptoCurrencyAmount.Token -> currencies.firstOrNull {
it is CryptoCurrency.Token &&
it.id.rawCurrencyId == amount.id &&
it.id.rawCurrencyId == amount.tokenId &&
it.contractAddress == amount.tokenContractAddress
}
}
currency?.id?.let { it to amount.value }
if (currency == null) {
Timber.e("Unable to find cryptocurrency for amount: $amount")
null
} else {
currency.id to amount.value
}
}
.toMap()
}
private fun formatTransactions(
networksAddresses: Set<String>,
transactions: Set<CryptoCurrencyTransaction>,
currencies: Set<CryptoCurrency>,
): Map<CryptoCurrency.ID, Set<PendingTransaction>> {
if (transactions.isEmpty()) return emptyMap()
return currencies
.asSequence()
.map { currency ->
val currencyTransactions = when (currency) {
is CryptoCurrency.Coin -> transactions.filterTo(hashSetOf()) { transaction ->
transaction is CryptoCurrencyTransaction.Coin
}
is CryptoCurrency.Token -> transactions.filterTo(hashSetOf()) { transaction ->
transaction is CryptoCurrencyTransaction.Token &&
transaction.tokenId == currency.id.rawCurrencyId &&
transaction.tokenContractAddress == currency.contractAddress
}
}
currency.id to createCurrentTransactions(networksAddresses, currencyTransactions)
}
.toMap()
}
private fun createCurrentTransactions(
networksAddresses: Set<String>,
transactions: Set<CryptoCurrencyTransaction>,
): Set<PendingTransaction> {
return transactions.mapNotNullTo(hashSetOf()) { createCurrentTransaction(networksAddresses, it) }
}
private fun createCurrentTransaction(
networksAddresses: Set<String>,
transaction: CryptoCurrencyTransaction,
): PendingTransaction? {
val direction = when {
transaction.toAddress in networksAddresses -> PendingTransaction.Direction.Incoming(
fromAddress = transaction.fromAddress,
)
transaction.fromAddress in networksAddresses -> PendingTransaction.Direction.Outgoing(
toAddress = transaction.toAddress,
)
else -> {
Timber.e(
"""
Unable to find transaction direction
|- To address: ${transaction.toAddress}
|- From address: ${transaction.fromAddress}
|- Network addresses: $networksAddresses
""".trimIndent(),
)
return null
}
}
return PendingTransaction(
amount = transaction.amount,
direction = direction,
sentAt = transaction.sentAt,
)
}
private fun getNetworkAddress(defaultAddress: String, availableAddresses: Set<String>): NetworkAddress {
return if (availableAddresses.size != 1) {
NetworkAddress.Selectable(defaultAddress, availableAddresses)
} else {
NetworkAddress.Single(defaultAddress)
}
}
}

View file

@ -30,6 +30,7 @@ dependencies {
implementation(deps.moshi.kotlin)
implementation(deps.timber)
implementation(deps.kotlin.coroutines)
implementation(deps.jodatime)
/** Testing libraries */
testImplementation(deps.test.junit)

View file

@ -9,7 +9,7 @@ sealed class CryptoCurrencyAmount {
data class Coin(override val value: BigDecimal) : CryptoCurrencyAmount()
data class Token(
val id: String?,
val tokenId: String?,
val tokenContractAddress: String,
override val value: BigDecimal,
) : CryptoCurrencyAmount()

View file

@ -0,0 +1,28 @@
package com.tangem.domain.walletmanager.model
import org.joda.time.DateTime
import java.math.BigDecimal
sealed class CryptoCurrencyTransaction {
abstract val amount: BigDecimal
abstract val fromAddress: String?
abstract val toAddress: String?
abstract val sentAt: DateTime
data class Coin(
override val amount: BigDecimal,
override val fromAddress: String?,
override val toAddress: String?,
override val sentAt: DateTime,
) : CryptoCurrencyTransaction()
data class Token(
val tokenId: String?,
val tokenContractAddress: String,
override val amount: BigDecimal,
override val fromAddress: String?,
override val toAddress: String?,
override val sentAt: DateTime,
) : CryptoCurrencyTransaction()
}

View file

@ -9,9 +9,15 @@ sealed class UpdateWalletManagerResult {
object Unreachable : UpdateWalletManagerResult()
data class Verified(
val tokensAmounts: Set<CryptoCurrencyAmount>,
val hasTransactionsInProgress: Boolean, // TODO: May be add recent transactions
val defaultAddress: String,
val addresses: Set<String>,
val currenciesAmounts: Set<CryptoCurrencyAmount>,
val currentTransactions: Set<CryptoCurrencyTransaction>,
) : UpdateWalletManagerResult()
data class NoAccount(val amountToCreateAccount: BigDecimal) : UpdateWalletManagerResult()
data class NoAccount(
val defaultAddress: String,
val addresses: Set<String>,
val amountToCreateAccount: BigDecimal,
) : UpdateWalletManagerResult()
}

View file

@ -1,31 +1,39 @@
package com.tangem.domain.walletmanager.utils
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.Address
import com.tangem.domain.common.extensions.amountToCreateAccount
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import org.joda.time.Instant
import timber.log.Timber
import java.math.BigDecimal
import java.util.Calendar
internal class UpdateWalletManagerResultFactory {
fun getResult(walletManager: WalletManager): UpdateWalletManagerResult.Verified {
val hasNotConfirmedTransactions = walletManager.wallet
.recentTransactions
.any { it.status != TransactionStatus.Confirmed }
val amounts = walletManager.wallet.amounts
val wallet = walletManager.wallet
return UpdateWalletManagerResult.Verified(
tokensAmounts = getTokensAmounts(amounts.values.toSet()),
hasTransactionsInProgress = hasNotConfirmedTransactions,
defaultAddress = wallet.address,
addresses = getAvailableAddresses(wallet.addresses),
currenciesAmounts = getTokensAmounts(wallet.amounts.values.toSet()),
currentTransactions = getCurrentTransactions(wallet.recentTransactions.toSet()),
)
}
fun getDemoResult(walletManager: WalletManager, demoAmount: Amount): UpdateWalletManagerResult.Verified {
val wallet = walletManager.wallet
return UpdateWalletManagerResult.Verified(
tokensAmounts = getDemoTokensAmounts(demoAmount, walletManager.cardTokens),
hasTransactionsInProgress = false,
defaultAddress = wallet.address,
addresses = getAvailableAddresses(wallet.addresses),
currenciesAmounts = getDemoTokensAmounts(demoAmount, walletManager.cardTokens),
currentTransactions = getCurrentTransactions(wallet.recentTransactions.toSet()),
)
}
@ -40,13 +48,17 @@ internal class UpdateWalletManagerResultFactory {
"Unable to get required amount to create account for: $blockchain"
}
return UpdateWalletManagerResult.NoAccount(amountToCreateAccount)
return UpdateWalletManagerResult.NoAccount(
defaultAddress = wallet.address,
addresses = getAvailableAddresses(wallet.addresses),
amountToCreateAccount = amountToCreateAccount,
)
}
private fun getTokensAmounts(amounts: Set<Amount>): Set<CryptoCurrencyAmount> {
val mutableAmounts = hashSetOf<CryptoCurrencyAmount>()
return amounts.mapNotNullTo(mutableAmounts, ::getTokenAmount)
return amounts.mapNotNullTo(mutableAmounts, ::createCurrencyAmount)
}
private fun getDemoTokensAmounts(demoAmount: Amount, tokens: Set<Token>): Set<CryptoCurrencyAmount> {
@ -58,27 +70,94 @@ internal class UpdateWalletManagerResultFactory {
}
}
private fun getTokenAmount(amount: Amount): CryptoCurrencyAmount? {
private fun getCurrentTransactions(recentTransactions: Set<TransactionData>): Set<CryptoCurrencyTransaction> {
val unconfirmedTransactions = recentTransactions.filter {
it.status == TransactionStatus.Unconfirmed
}
return unconfirmedTransactions.mapNotNullTo(hashSetOf(), ::createCurrencyTransaction)
}
private fun createCurrencyAmount(amount: Amount): CryptoCurrencyAmount? {
return when (val type = amount.type) {
is AmountType.Token -> CryptoCurrencyAmount.Token(
id = type.token.id,
tokenId = type.token.id,
tokenContractAddress = type.token.contractAddress,
value = getAmountValue(amount) ?: return null,
value = getCurrencyAmountValue(amount) ?: return null,
)
is AmountType.Coin -> CryptoCurrencyAmount.Coin(
value = getAmountValue(amount) ?: return null,
value = getCurrencyAmountValue(amount) ?: return null,
)
is AmountType.Reserve -> null
}
}
private fun getAmountValue(amount: Amount): BigDecimal? {
private fun createCurrencyTransaction(data: TransactionData): CryptoCurrencyTransaction? {
val fromAddress = takeAddressIfNotUnknown(data.sourceAddress)
val toAddress = takeAddressIfNotUnknown(data.destinationAddress)
val amount = getTransactionAmountValue(data.amount) ?: return null
val sentAt = getTransactionSentTime(data.date) ?: return null
return when (val type = data.amount.type) {
is AmountType.Coin -> CryptoCurrencyTransaction.Coin(
amount = amount,
fromAddress = fromAddress,
toAddress = toAddress,
sentAt = sentAt,
)
is AmountType.Token -> CryptoCurrencyTransaction.Token(
tokenId = type.token.id,
tokenContractAddress = type.token.contractAddress,
amount = amount,
fromAddress = fromAddress,
toAddress = toAddress,
sentAt = sentAt,
)
is AmountType.Reserve -> null
}
}
private fun getAvailableAddresses(addresses: Set<Address>): Set<String> {
return addresses.mapTo(hashSetOf()) { it.value }
}
private fun getCurrencyAmountValue(amount: Amount): BigDecimal? {
val value = amount.value
if (value == null) {
Timber.e("Amount not found for currency: ${amount.currencySymbol}")
Timber.e("Currency amount must not be null: ${amount.currencySymbol}")
}
return value
}
private fun getTransactionAmountValue(amount: Amount): BigDecimal? {
val value = amount.value
if (value == null) {
Timber.e("Transaction amount must not be null: ${amount.currencySymbol}")
}
return value
}
private fun getTransactionSentTime(date: Calendar?): DateTime? {
if (date == null) {
Timber.e("Transaction date must not be null")
return null
}
val instant = Instant.ofEpochMilli(date.timeInMillis)
val timeZone = DateTimeZone.forTimeZone(date.timeZone)
return instant.toDateTime(timeZone)
}
private fun takeAddressIfNotUnknown(address: String): String? {
return address.takeIf { it.isNotBlank() && it != UNKNOWN_TRANSACTION_ADDRESS }
}
private companion object {
const val UNKNOWN_TRANSACTION_ADDRESS = "unknown"
}
}

View file

@ -14,6 +14,9 @@ dependencies {
/** Project - Other */
implementation(projects.core.utils)
/** Utils */
implementation(deps.jodatime)
/** Tests */
testImplementation(deps.test.junit)
testImplementation(deps.test.coroutine)

View file

@ -35,8 +35,11 @@ data class CryptoCurrencyStatus(
/** The change in price of the token. */
open val priceChange: BigDecimal? = null
/** Indicates if there are any transactions in progress related to the token. */
open val hasTransactionsInProgress: Boolean = false
/** Indicates if there are any transactions in progress related to the cryptocurrency network. */
open val hasCurrentNetworkTransactions: Boolean = false
/** The pending cryptocurrency transactions. */
open val pendingTransactions: Set<PendingTransaction> = emptySet()
}
/** Represents the Loading state of a token, typically while fetching its details. */
@ -58,15 +61,17 @@ data class CryptoCurrencyStatus(
* @property fiatAmount The fiat equivalent of the token's amount.
* @property fiatRate The exchange rate used for converting the token amount to fiat.
* @property priceChange The change in price of the token.
* @property hasTransactionsInProgress Indicates if there are any transactions in progress related to the token
* network.
* @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the
* cryptocurrency network.
* @property pendingTransactions The current cryptocurrency transactions.
*/
data class Loaded(
override val amount: BigDecimal,
override val fiatAmount: BigDecimal,
override val fiatRate: BigDecimal,
override val priceChange: BigDecimal,
override val hasTransactionsInProgress: Boolean,
override val hasCurrentNetworkTransactions: Boolean,
override val pendingTransactions: Set<PendingTransaction>,
) : Status()
/**
@ -76,25 +81,30 @@ data class CryptoCurrencyStatus(
* @property fiatAmount The fiat equivalent of the token's amount (optional).
* @property fiatRate The exchange rate used for converting the token amount to fiat (optional).
* @property priceChange The change in price of the token (optional).
* @property hasTransactionsInProgress Indicates if there are any transactions in progress related to the token
* network.
* @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the
* cryptocurrency network.
* @property pendingTransactions The current cryptocurrency transactions.
*/
data class Custom(
override val amount: BigDecimal,
override val fiatAmount: BigDecimal?,
override val fiatRate: BigDecimal?,
override val priceChange: BigDecimal?,
override val hasTransactionsInProgress: Boolean,
override val hasCurrentNetworkTransactions: Boolean,
override val pendingTransactions: Set<PendingTransaction>,
) : Status()
/**
* Represents a state where the token is available, but there is no current quote available for it.
*
* @property amount The amount of the token.
* @property hasTransactionsInProgress Indicates if there are any transactions in progress related to the token.
* @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the
* cryptocurrency network.
* @property pendingTransactions The current cryptocurrency transactions.
*/
data class NoQuote(
override val amount: BigDecimal,
override val hasTransactionsInProgress: Boolean,
override val hasCurrentNetworkTransactions: Boolean,
override val pendingTransactions: Set<PendingTransaction>,
) : Status()
}

View file

@ -0,0 +1,43 @@
package com.tangem.domain.tokens.model
/**
* Represents a network address configuration.
*/
sealed class NetworkAddress {
/** The default or currently selected network address. */
abstract val defaultAddress: String
/**
* Represents a single static network address.
*
* @property defaultAddress The static network address.
*/
data class Single(override val defaultAddress: String) : NetworkAddress() {
init {
checkDefaultAddress()
}
}
/**
* Represents a network configuration where an address can be chosen from a set of available addresses.
*
* @property defaultAddress The currently selected or default network address.
* @property availableAddresses The set of available network addresses to choose from.
*/
data class Selectable(
override val defaultAddress: String,
val availableAddresses: Set<String>,
) : NetworkAddress() {
init {
checkDefaultAddress()
require(availableAddresses.isNotEmpty()) { "Available network addresses must not be empty" }
}
}
protected fun checkDefaultAddress() {
require(defaultAddress.isNotBlank()) { "Selected network address must not be blank" }
}
}

View file

@ -33,20 +33,28 @@ data class NetworkStatus(
object MissedDerivation : Status()
/**
* Represents the verified state of the network, including the amounts associated with different cryptocurrencies and whether there are transactions in progress.
* Represents the verified state of the network, including the amounts associated with different cryptocurrencies
* and whether there are transactions in progress.
*
* @property address Network addresses.
* @property amounts A map containing the amounts associated with different cryptocurrencies within the network.
* @property hasTransactionsInProgress A boolean indicating whether there are transactions in progress within the network.
* @property pendingTransactions A map containing pending transactions associated with different cryptocurrencies
* within the network.
*/
data class Verified(
val address: NetworkAddress,
val amounts: Map<CryptoCurrency.ID, BigDecimal>,
val hasTransactionsInProgress: Boolean,
val pendingTransactions: Map<CryptoCurrency.ID, Set<PendingTransaction>>,
) : Status()
/**
* Represents the state where there is no account, and an amount is required to create one.
*
* @property address Network addresses.
* @property amountToCreateAccount The amount required to create an account within the network.
*/
data class NoAccount(val amountToCreateAccount: BigDecimal) : Status()
data class NoAccount(
val address: NetworkAddress,
val amountToCreateAccount: BigDecimal,
) : Status()
}

View file

@ -0,0 +1,40 @@
package com.tangem.domain.tokens.model
import org.joda.time.DateTime
import java.math.BigDecimal
/**
* Represents a cryptocurrency transaction that is currently in progress.
*
* @property amount The monetary amount involved in the transaction.
* @property direction The direction of the transaction, indicating if it's an incoming or outgoing transaction.
* @property sentAt The timestamp when the transaction was executed.
*/
data class PendingTransaction(
val amount: BigDecimal,
val direction: Direction,
val sentAt: DateTime,
) {
/**
* Represents the direction of the transaction.
*/
sealed class Direction {
/**
* Represents an incoming transaction.
*
* @property fromAddress The source address from which the assets are being received. May be `null` if
* transaction received from unknown address.
*/
data class Incoming(val fromAddress: String?) : Direction()
/**
* Represents an outgoing transaction.
*
* @property toAddress The destination address to which the assets are being sent. May be `null` if transaction
* sent to unknown address.
*/
data class Outgoing(val toAddress: String?) : Direction()
}
}

View file

@ -27,18 +27,22 @@ internal class CurrencyStatusOperations(
private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Status {
val amount = status.amounts[currency.id] ?: return CryptoCurrencyStatus.Unreachable
val hasCurrentNetworkTransactions = status.pendingTransactions.isNotEmpty()
val currentTransactions = status.pendingTransactions.getOrElse(currency.id, ::emptySet)
return when {
ignoreQuote -> CryptoCurrencyStatus.NoQuote(
amount = amount,
hasTransactionsInProgress = status.hasTransactionsInProgress,
hasCurrentNetworkTransactions = hasCurrentNetworkTransactions,
pendingTransactions = currentTransactions,
)
currency is CryptoCurrency.Token && currency.isCustom -> CryptoCurrencyStatus.Custom(
amount = amount,
fiatAmount = calculateFiatAmountOrNull(amount, quote?.fiatRate),
fiatRate = quote?.fiatRate,
priceChange = quote?.priceChange,
hasTransactionsInProgress = status.hasTransactionsInProgress,
hasCurrentNetworkTransactions = hasCurrentNetworkTransactions,
pendingTransactions = currentTransactions,
)
quote == null -> CryptoCurrencyStatus.Loading
else -> CryptoCurrencyStatus.Loaded(
@ -46,7 +50,8 @@ internal class CurrencyStatusOperations(
fiatAmount = calculateFiatAmount(amount, quote.fiatRate),
fiatRate = quote.fiatRate,
priceChange = quote.priceChange,
hasTransactionsInProgress = status.hasTransactionsInProgress,
hasCurrentNetworkTransactions = hasCurrentNetworkTransactions,
pendingTransactions = currentTransactions,
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.domain.tokens.mock
import arrow.core.NonEmptySet
import arrow.core.nonEmptySetOf
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.models.Network
import java.math.BigDecimal
@ -48,6 +49,7 @@ internal object MockNetworks {
networkId = network3.id,
value = NetworkStatus.NoAccount(
amountToCreateAccount = amountToCreateAccount,
address = NetworkAddress.Single(defaultAddress = "mock"),
),
)
@ -61,7 +63,8 @@ internal object MockNetworks {
MockTokens.token2.id to BigDecimal.TEN,
MockTokens.token3.id to BigDecimal.TEN,
),
hasTransactionsInProgress = false,
pendingTransactions = emptyMap(),
address = NetworkAddress.Single(defaultAddress = "mock"),
),
)
@ -73,7 +76,8 @@ internal object MockNetworks {
MockTokens.token5.id to BigDecimal.TEN,
MockTokens.token6.id to BigDecimal.TEN,
),
hasTransactionsInProgress = false,
pendingTransactions = emptyMap(),
address = NetworkAddress.Single(defaultAddress = "mock"),
),
)
@ -86,7 +90,8 @@ internal object MockNetworks {
MockTokens.token9.id to BigDecimal.TEN,
MockTokens.token10.id to BigDecimal.TEN,
),
hasTransactionsInProgress = false,
pendingTransactions = emptyMap(),
address = NetworkAddress.Single(defaultAddress = "mock"),
),
)

View file

@ -83,7 +83,8 @@ internal object MockTokensStates {
fiatAmount = fiatAmount,
fiatRate = quote.fiatRate,
priceChange = quote.priceChange,
hasTransactionsInProgress = false,
pendingTransactions = emptySet(),
hasCurrentNetworkTransactions = false,
),
)
}
@ -92,7 +93,8 @@ internal object MockTokensStates {
currency.copy(
value = CryptoCurrencyStatus.NoQuote(
amount = currency.value.amount!!,
hasTransactionsInProgress = false,
pendingTransactions = emptySet(),
hasCurrentNetworkTransactions = false,
),
)
}

View file

@ -41,7 +41,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter(
tokenIconResId = currency.iconResId,
networkBadgeIconResId = currency.networkBadgeIconResId,
amount = getFormattedAmount(),
hasPending = value.hasTransactionsInProgress,
hasPending = value.hasCurrentNetworkTransactions,
tokenOptions = if (isWalletContentHidden) {
TokenItemState.TokenOptionsState.Hidden(getPriceChangeConfig())
} else {