Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-05 11:26:59 +05:00
parent 164b6e3c71
commit 9cac096ce2
14 changed files with 239 additions and 113 deletions

View file

@ -17,6 +17,7 @@ dependencies {
implementation(projects.domain.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.wallets.models)
/** Project - Data */

View file

@ -2,9 +2,9 @@ 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.txhistory.models.TxHistoryItem
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
@ -31,7 +31,6 @@ internal class NetworkStatusFactory {
address = getNetworkAddress(result.defaultAddress, result.addresses),
amounts = formatAmounts(result.currenciesAmounts, currencies),
pendingTransactions = formatTransactions(
networksAddresses = result.addresses,
transactions = result.currentTransactions,
currencies = currencies,
),
@ -67,10 +66,9 @@ internal class NetworkStatusFactory {
}
private fun formatTransactions(
networksAddresses: Set<String>,
transactions: Set<CryptoCurrencyTransaction>,
currencies: Set<CryptoCurrency>,
): Map<CryptoCurrency.ID, Set<PendingTransaction>> {
): Map<CryptoCurrency.ID, Set<TxHistoryItem>> {
if (transactions.isEmpty()) return emptyMap()
return currencies
@ -87,48 +85,13 @@ internal class NetworkStatusFactory {
}
}
currency.id to createCurrentTransactions(networksAddresses, currencyTransactions)
currency.id to createCurrentTransactions(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 createCurrentTransactions(transactions: Set<CryptoCurrencyTransaction>): Set<TxHistoryItem> {
return transactions.mapTo(hashSetOf()) { it.txHistoryItem }
}
private fun getNetworkAddress(defaultAddress: String, availableAddresses: Set<String>): NetworkAddress {

View file

@ -1,28 +1,17 @@
package com.tangem.domain.walletmanager.model
import org.joda.time.DateTime
import java.math.BigDecimal
import com.tangem.domain.txhistory.models.TxHistoryItem
// TODO: [REDACTED_JIRA] move to txhistory module
sealed class CryptoCurrencyTransaction {
abstract val amount: BigDecimal
abstract val fromAddress: String?
abstract val toAddress: String?
abstract val sentAt: DateTime
abstract val txHistoryItem: TxHistoryItem
data class Coin(
override val amount: BigDecimal,
override val fromAddress: String?,
override val toAddress: String?,
override val sentAt: DateTime,
) : CryptoCurrencyTransaction()
data class Coin(override val txHistoryItem: TxHistoryItem) : 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,
override val txHistoryItem: TxHistoryItem,
) : CryptoCurrencyTransaction()
}

View file

@ -3,37 +3,37 @@ 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.txhistory.models.TxHistoryItem
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
import java.util.concurrent.TimeUnit
internal class UpdateWalletManagerResultFactory {
fun getResult(walletManager: WalletManager): UpdateWalletManagerResult.Verified {
val wallet = walletManager.wallet
val addresses = getAvailableAddresses(wallet.addresses)
return UpdateWalletManagerResult.Verified(
defaultAddress = wallet.address,
addresses = getAvailableAddresses(wallet.addresses),
addresses = addresses,
currenciesAmounts = getTokensAmounts(wallet.amounts.values.toSet()),
currentTransactions = getCurrentTransactions(wallet.recentTransactions.toSet()),
currentTransactions = getCurrentTransactions(addresses, wallet.recentTransactions.toSet()),
)
}
fun getDemoResult(walletManager: WalletManager, demoAmount: Amount): UpdateWalletManagerResult.Verified {
val wallet = walletManager.wallet
val addresses = getAvailableAddresses(wallet.addresses)
return UpdateWalletManagerResult.Verified(
defaultAddress = wallet.address,
addresses = getAvailableAddresses(wallet.addresses),
addresses = addresses,
currenciesAmounts = getDemoTokensAmounts(demoAmount, walletManager.cardTokens),
currentTransactions = getCurrentTransactions(wallet.recentTransactions.toSet()),
currentTransactions = getCurrentTransactions(addresses, wallet.recentTransactions.toSet()),
)
}
@ -70,12 +70,15 @@ internal class UpdateWalletManagerResultFactory {
}
}
private fun getCurrentTransactions(recentTransactions: Set<TransactionData>): Set<CryptoCurrencyTransaction> {
private fun getCurrentTransactions(
walletAddresses: Set<String>,
recentTransactions: Set<TransactionData>,
): Set<CryptoCurrencyTransaction> {
val unconfirmedTransactions = recentTransactions.filter {
it.status == TransactionStatus.Unconfirmed
}
return unconfirmedTransactions.mapNotNullTo(hashSetOf(), ::createCurrencyTransaction)
return unconfirmedTransactions.mapNotNullTo(hashSetOf()) { createCurrencyTransaction(walletAddresses, it) }
}
private fun createCurrencyAmount(amount: Amount): CryptoCurrencyAmount? {
@ -92,31 +95,78 @@ internal class UpdateWalletManagerResultFactory {
}
}
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
private fun createCurrencyTransaction(
walletAddresses: Set<String>,
data: TransactionData,
): CryptoCurrencyTransaction? {
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.Coin -> {
val txHistoryItem = createTxHistoryItem(walletAddresses, data) ?: return null
CryptoCurrencyTransaction.Coin(txHistoryItem)
}
is AmountType.Token -> {
val txHistoryItem = createTxHistoryItem(walletAddresses, data) ?: return null
CryptoCurrencyTransaction.Token(
tokenId = type.token.id,
tokenContractAddress = type.token.contractAddress,
txHistoryItem = txHistoryItem,
)
}
is AmountType.Reserve -> null
}
}
private fun createTxHistoryItem(walletAddresses: Set<String>, data: TransactionData): TxHistoryItem? {
val direction = extractDirection(walletAddresses, data) ?: run {
Timber.w("Can not determine address for $data")
return null
}
val hash = data.hash ?: return null
val millis = data.date?.timeInMillis ?: return null
val amount = getTransactionAmountValue(data.amount) ?: return null
return TxHistoryItem(
txHash = hash,
timestampInMillis = TimeUnit.SECONDS.toMillis(millis),
direction = direction,
status = when (data.status) {
TransactionStatus.Confirmed -> TxHistoryItem.TxStatus.Confirmed
TransactionStatus.Unconfirmed -> TxHistoryItem.TxStatus.Unconfirmed
},
type = TxHistoryItem.TransactionType.Transfer,
amount = amount,
)
}
private fun extractDirection(
walletAddresses: Set<String>,
data: TransactionData,
): TxHistoryItem.TransactionDirection? {
val fromAddress = data.sourceAddress
val toAddress = data.destinationAddress
return when {
toAddress in walletAddresses -> {
TxHistoryItem.TransactionDirection.Incoming(TxHistoryItem.Address.Single(fromAddress))
}
fromAddress in walletAddresses -> {
TxHistoryItem.TransactionDirection.Outgoing(TxHistoryItem.Address.Single(toAddress))
}
else -> {
Timber.e(
"""
Unable to find transaction direction
|- To address: ${data.destinationAddress}
|- From address: ${data.sourceAddress}
|- Network addresses: $walletAddresses
""".trimIndent(),
)
return null
}
}
}
private fun getAvailableAddresses(addresses: Set<Address>): Set<String> {
return addresses.mapTo(hashSetOf()) { it.value }
}
@ -140,24 +190,4 @@ internal class UpdateWalletManagerResultFactory {
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

@ -15,6 +15,7 @@ dependencies {
implementation(projects.domain.models)
implementation(projects.domain.legacy)
implementation(projects.domain.tokens.models)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.appCurrency.models)

View file

@ -1,6 +1,7 @@
package com.tangem.domain.tokens.model
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.txhistory.models.TxHistoryItem
import java.math.BigDecimal
/**
@ -39,7 +40,7 @@ data class CryptoCurrencyStatus(
open val hasCurrentNetworkTransactions: Boolean = false
/** The pending cryptocurrency transactions. */
open val pendingTransactions: Set<PendingTransaction> = emptySet()
open val pendingTransactions: Set<TxHistoryItem> = emptySet()
/** The network address */
open val networkAddress: NetworkAddress? = null
@ -74,7 +75,7 @@ data class CryptoCurrencyStatus(
override val fiatRate: BigDecimal,
override val priceChange: BigDecimal,
override val hasCurrentNetworkTransactions: Boolean,
override val pendingTransactions: Set<PendingTransaction>,
override val pendingTransactions: Set<TxHistoryItem>,
override val networkAddress: NetworkAddress?,
) : Status()
@ -95,7 +96,7 @@ data class CryptoCurrencyStatus(
override val fiatRate: BigDecimal?,
override val priceChange: BigDecimal?,
override val hasCurrentNetworkTransactions: Boolean,
override val pendingTransactions: Set<PendingTransaction>,
override val pendingTransactions: Set<TxHistoryItem>,
override val networkAddress: NetworkAddress?,
) : Status()
@ -110,7 +111,7 @@ data class CryptoCurrencyStatus(
data class NoQuote(
override val amount: BigDecimal,
override val hasCurrentNetworkTransactions: Boolean,
override val pendingTransactions: Set<PendingTransaction>,
override val pendingTransactions: Set<TxHistoryItem>,
override val networkAddress: NetworkAddress?,
) : Status()
}

View file

@ -2,6 +2,7 @@ package com.tangem.domain.tokens.model
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.Network
import com.tangem.domain.txhistory.models.TxHistoryItem
import java.math.BigDecimal
/**
@ -44,7 +45,7 @@ data class NetworkStatus(
data class Verified(
val address: NetworkAddress,
val amounts: Map<CryptoCurrency.ID, BigDecimal>,
val pendingTransactions: Map<CryptoCurrency.ID, Set<PendingTransaction>>,
val pendingTransactions: Map<CryptoCurrency.ID, Set<TxHistoryItem>>,
) : Status()
/**

View file

@ -82,5 +82,6 @@ internal object TokenDetailsPreviewData {
),
),
dialogConfig = null,
pendingTxs = persistentListOf(),
)
}

View file

@ -1,14 +1,17 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig
import kotlinx.collections.immutable.PersistentList
internal data class TokenDetailsState(
val topAppBarConfig: TokenDetailsTopAppBarConfig,
val tokenInfoBlockState: TokenInfoBlockState,
val tokenBalanceBlockState: TokenDetailsBalanceBlockState,
val marketPriceBlockState: MarketPriceBlockState,
val pendingTxs: PersistentList<TransactionState>,
val txHistoryState: TxHistoryState,
val dialogConfig: TokenDetailsDialogConfig?,
)

View file

@ -10,14 +10,22 @@ import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryToTransactionStateConverter
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
internal class TokenDetailsLoadedBalanceConverter(
private val currentStateProvider: Provider<TokenDetailsState>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val symbol: String,
private val decimals: Int,
) : Converter<Either<CurrencyStatusError, CryptoCurrencyStatus>, TokenDetailsState> {
private val txHistoryItemConverter by lazy {
TokenDetailsTxHistoryToTransactionStateConverter(symbol, decimals)
}
override fun convert(value: Either<CurrencyStatusError, CryptoCurrencyStatus>): TokenDetailsState {
return value.fold(ifLeft = { convertError() }, ifRight = ::convert)
}
@ -33,6 +41,7 @@ internal class TokenDetailsLoadedBalanceConverter(
return state.copy(
tokenBalanceBlockState = getBalanceState(state.tokenBalanceBlockState, status),
marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName),
pendingTxs = status.value.pendingTransactions.map(txHistoryItemConverter::convert).toPersistentList(),
)
}

View file

@ -42,6 +42,7 @@ internal class TokenDetailsSkeletonStateConverter(
actionButtons = createButtons(),
),
marketPriceBlockState = MarketPriceBlockState.Loading(value.cryptoCurrency.name),
pendingTxs = persistentListOf(),
txHistoryState = TxHistoryState.Content(
contentItems = MutableStateFlow(
value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick),

View file

@ -34,6 +34,8 @@ internal class TokenDetailsStateFactory(
TokenDetailsLoadedBalanceConverter(
currentStateProvider = currentStateProvider,
appCurrencyProvider = appCurrencyProvider,
symbol = symbol,
decimals = decimals,
)
}

View file

@ -0,0 +1,94 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.converter.Converter
import com.tangem.utils.toBriefAddressFormat
import com.tangem.utils.toFormattedCurrencyString
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import org.joda.time.format.DateTimeFormatterBuilder
import java.math.BigDecimal
import java.util.Locale
internal class TokenDetailsTxHistoryToTransactionStateConverter(
private val symbol: String,
private val decimals: Int,
) : Converter<TxHistoryItem, TransactionState> {
/** Example, 13:35 */
private val timeFormatter by lazy {
DateTimeFormatterBuilder()
.appendHourOfDay(1)
.appendLiteral(':')
.appendMinuteOfHour(2)
.toFormatter()
.withLocale(Locale.getDefault())
}
override fun convert(value: TxHistoryItem): TransactionState {
return when (value.type) {
TxHistoryItem.TransactionType.Transfer -> {
when (val direction = value.direction) {
is TxHistoryItem.TransactionDirection.Incoming -> {
createIncomingTransferTransaction(value, direction)
}
is TxHistoryItem.TransactionDirection.Outgoing -> {
createOutgoingTransferTransaction(value, direction)
}
}
}
}
}
private fun createIncomingTransferTransaction(
item: TxHistoryItem,
direction: TxHistoryItem.TransactionDirection.Incoming,
): TransactionState {
return when (item.status) {
TxHistoryItem.TxStatus.Confirmed -> TransactionState.Receive(
txHash = item.txHash,
address = direction.extractAddress(),
amount = item.amount.toCryptoCurrencyFormat(),
timestamp = timeFormatter.print(DateTime(item.timestampInMillis, DateTimeZone.getDefault())),
)
TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Receiving(
txHash = item.txHash,
address = direction.extractAddress(),
amount = item.amount.toCryptoCurrencyFormat(),
timestamp = timeFormatter.print(DateTime(item.timestampInMillis, DateTimeZone.getDefault())),
)
}
}
private fun createOutgoingTransferTransaction(
item: TxHistoryItem,
direction: TxHistoryItem.TransactionDirection.Outgoing,
): TransactionState {
return when (item.status) {
TxHistoryItem.TxStatus.Confirmed -> TransactionState.Send(
txHash = item.txHash,
address = direction.extractAddress(),
amount = item.amount.toCryptoCurrencyFormat(),
timestamp = timeFormatter.print(DateTime(item.timestampInMillis, DateTimeZone.getDefault())),
)
TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Sending(
txHash = item.txHash,
address = direction.extractAddress(),
amount = item.amount.toCryptoCurrencyFormat(),
timestamp = timeFormatter.print(DateTime(item.timestampInMillis, DateTimeZone.getDefault())),
)
}
}
private fun BigDecimal.toCryptoCurrencyFormat(): String {
return toFormattedCurrencyString(currency = symbol, decimals = decimals)
}
private fun TxHistoryItem.TransactionDirection.extractAddress(): TextReference = when (val addr = address) {
TxHistoryItem.Address.Multiple -> TextReference.Res(R.string.transaction_history_multiple_addresses)
is TxHistoryItem.Address.Single -> TextReference.Str(addr.rawAddress.toBriefAddressFormat())
}
}

View file

@ -1,15 +1,23 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.util.fastForEach
import androidx.paging.compose.collectAsLazyPagingItems
import com.tangem.core.ui.components.marketprice.MarketPriceBlock
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.Transaction
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.components.transactions.txHistoryItems
import com.tangem.core.ui.res.TangemTheme
@ -19,6 +27,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock
import kotlinx.collections.immutable.PersistentList
@Composable
internal fun TokenDetailsScreen(state: TokenDetailsState) {
@ -55,6 +64,14 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
contentType = MarketPriceBlockState::class.java,
content = { MarketPriceBlock(modifier = itemModifier, state = state.marketPriceBlockState) },
)
if (state.txHistoryState is TxHistoryState.NotSupported && state.pendingTxs.isNotEmpty()) {
item {
PendingTxsBlock(
pendingTxs = state.pendingTxs,
modifier = itemModifier,
)
}
}
txHistoryItems(state = state.txHistoryState, txHistoryItems = txHistoryItems)
}
@ -62,6 +79,19 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
}
}
@Composable
private fun PendingTxsBlock(pendingTxs: PersistentList<TransactionState>, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
.background(color = TangemTheme.colors.background.primary),
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
horizontalAlignment = Alignment.Start,
) {
pendingTxs.fastForEach { Transaction(state = it) }
}
}
@Preview
@Composable
private fun Preview_TokenDetailsScreen_LightTheme() {