Updated on 2026-08-14
This commit is contained in:
parent
ef8a13cfaa
commit
2f60ccf3fc
14 changed files with 732 additions and 148 deletions
|
|
@ -2051,6 +2051,8 @@
|
|||
<string name="twins_recreate_toolbar">Tangem Twin</string>
|
||||
<string name="twins_recreate_warning">This action is irreversible. You will not have access to the old wallet.</string>
|
||||
<string name="twins_scan_twin_with_number">Tap the twin card with number %s and do not remove until the end of the operation</string>
|
||||
<string name="tx_history_onramp_top_up">Top up</string>
|
||||
<string name="tx_history_onramp_topped_up">Topped up</string>
|
||||
<string name="unexpected_error_description">Please try again later. If the issue persists, please contact support.</string>
|
||||
<string name="unexpected_error_title">Something went wrong!</string>
|
||||
<string name="universal_error">We\'ve encountered an error. Error code: %s. Please contact our support.</string>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -32,6 +33,8 @@ import androidx.compose.ui.text.style.TextDecoration
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.icons.identicon.IdentIcon
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Direction
|
||||
|
|
@ -70,55 +73,92 @@ fun TransactionItem(state: TransactionItemUM, isBalanceHidden: Boolean, modifier
|
|||
|
||||
@Composable
|
||||
private fun ContentItem(state: TransactionItemUM.Content, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
|
||||
val rowModifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = state.onClick)
|
||||
.testTag(TransactionHistoryItemTestTags.ITEM)
|
||||
|
||||
TangemRowContainer(
|
||||
modifier = rowModifier,
|
||||
contentPadding = PaddingValues(
|
||||
horizontal = TangemTheme.dimens2.x4,
|
||||
vertical = TangemTheme.dimens2.x3,
|
||||
),
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = state.onClick)
|
||||
.testTag(TransactionHistoryItemTestTags.ITEM),
|
||||
) {
|
||||
StatusCircle(
|
||||
iconRes = state.iconRes,
|
||||
status = state.status,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.HEAD)
|
||||
.padding(end = TangemTheme.dimens2.x3)
|
||||
.size(TangemTheme.dimens2.x10)
|
||||
.testTag(TransactionHistoryItemTestTags.STATUS_PREFIX + state.status.testTagSuffix),
|
||||
TangemRowContainer(
|
||||
contentPadding = PaddingValues(
|
||||
horizontal = TangemTheme.dimens2.x4,
|
||||
vertical = TangemTheme.dimens2.x3,
|
||||
),
|
||||
) {
|
||||
StatusCircle(
|
||||
iconRes = state.iconRes,
|
||||
status = state.status,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.HEAD)
|
||||
.padding(end = TangemTheme.dimens2.x3)
|
||||
.size(TangemTheme.dimens2.x10)
|
||||
.testTag(TransactionHistoryItemTestTags.STATUS_PREFIX + state.status.testTagSuffix),
|
||||
)
|
||||
TitleText(
|
||||
title = state.title,
|
||||
status = state.status,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.START_TOP)
|
||||
.testTag(TransactionHistoryItemTestTags.TITLE),
|
||||
)
|
||||
SubtitleText(
|
||||
subtitle = state.subtitle,
|
||||
status = state.status,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.START_BOTTOM)
|
||||
.padding(top = TangemTheme.dimens2.x0_5),
|
||||
)
|
||||
state.amount?.let { amount ->
|
||||
AmountText(
|
||||
amount = amount,
|
||||
status = state.status,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.END_TOP)
|
||||
.testTag(TransactionHistoryItemTestTags.AMOUNT),
|
||||
)
|
||||
}
|
||||
CurrencyText(
|
||||
symbol = state.currencySymbol,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.END_BOTTOM)
|
||||
.padding(top = TangemTheme.dimens2.x0_5)
|
||||
.testTag(TransactionHistoryItemTestTags.CURRENCY),
|
||||
)
|
||||
}
|
||||
state.warning?.let { warning ->
|
||||
WarningLine(
|
||||
warning = warning,
|
||||
modifier = Modifier.padding(
|
||||
start = TangemTheme.dimens2.x4,
|
||||
end = TangemTheme.dimens2.x4,
|
||||
bottom = TangemTheme.dimens2.x3,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WarningLine(warning: TextReference, modifier: Modifier = Modifier) {
|
||||
val attention = TangemTheme.colors2.text.status.attention
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_alert_triangle_20),
|
||||
contentDescription = null,
|
||||
tint = attention,
|
||||
modifier = Modifier.size(TangemTheme.dimens2.x5),
|
||||
)
|
||||
TitleText(
|
||||
title = state.title,
|
||||
status = state.status,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.START_TOP)
|
||||
.testTag(TransactionHistoryItemTestTags.TITLE),
|
||||
)
|
||||
SubtitleText(
|
||||
subtitle = state.subtitle,
|
||||
status = state.status,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.START_BOTTOM)
|
||||
.padding(top = TangemTheme.dimens2.x0_5),
|
||||
)
|
||||
AmountText(
|
||||
amount = state.amount,
|
||||
status = state.status,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.END_TOP)
|
||||
.testTag(TransactionHistoryItemTestTags.AMOUNT),
|
||||
)
|
||||
CurrencyText(
|
||||
symbol = state.currencySymbol,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.END_BOTTOM)
|
||||
.padding(top = TangemTheme.dimens2.x0_5)
|
||||
.testTag(TransactionHistoryItemTestTags.CURRENCY),
|
||||
Text(
|
||||
text = warning.resolveReference(),
|
||||
color = attention,
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -262,6 +302,21 @@ private fun SubtitleText(subtitle: ContentSubtitle, status: Status, modifier: Mo
|
|||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
is ContentSubtitle.Asset -> InlineImageSubtitle(
|
||||
template = stringResourceSafe(subtitle.direction.templateResId(), subtitle.symbol),
|
||||
color = tertiary,
|
||||
afterIconColor = if (isFailed) tertiary else primary,
|
||||
modifier = modifier,
|
||||
) {
|
||||
subtitle.icon?.let { iconState ->
|
||||
CurrencyIcon(
|
||||
state = iconState,
|
||||
shouldDisplayNetwork = false,
|
||||
withFixedSize = false,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -491,4 +546,73 @@ private fun Preview_TransactionItem_Swap() {
|
|||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_TransactionItem_Express() {
|
||||
TangemThemePreviewRedesign {
|
||||
PreviewColumn(
|
||||
items = listOf(
|
||||
TransactionItemUM.Content(
|
||||
txHash = "exp-swap-u",
|
||||
amount = "-390.00",
|
||||
currencySymbol = "USDT",
|
||||
time = "",
|
||||
status = Status.Unconfirmed,
|
||||
direction = Direction.OUTGOING,
|
||||
onClick = {},
|
||||
iconRes = R.drawable.ic_exchange_vertical_24,
|
||||
title = stringReference("Swapping"),
|
||||
subtitle = ContentSubtitle.Asset(
|
||||
direction = ContentSubtitle.Direction.TO,
|
||||
symbol = "POL",
|
||||
icon = CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = R.drawable.ic_custom_token_44,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
),
|
||||
timestamp = 0L,
|
||||
warning = stringReference("KYC verification required by provider"),
|
||||
),
|
||||
TransactionItemUM.Content(
|
||||
txHash = "exp-onramp-c",
|
||||
amount = "+0.006339",
|
||||
currencySymbol = "BTC",
|
||||
time = "",
|
||||
status = Status.Confirmed,
|
||||
direction = Direction.INCOMING,
|
||||
onClick = {},
|
||||
iconRes = R.drawable.ic_tangem_card_24,
|
||||
title = stringReference("Topped up"),
|
||||
subtitle = ContentSubtitle.Asset(
|
||||
direction = ContentSubtitle.Direction.FROM,
|
||||
symbol = "SEK",
|
||||
icon = null,
|
||||
),
|
||||
timestamp = 0L,
|
||||
),
|
||||
TransactionItemUM.Content(
|
||||
txHash = "exp-onramp-f",
|
||||
amount = "0.006339",
|
||||
currencySymbol = "BTC",
|
||||
time = "",
|
||||
status = Status.Failed,
|
||||
direction = Direction.INCOMING,
|
||||
onClick = {},
|
||||
iconRes = R.drawable.ic_tangem_card_24,
|
||||
title = stringReference("Top up failed"),
|
||||
subtitle = ContentSubtitle.Asset(
|
||||
direction = ContentSubtitle.Direction.FROM,
|
||||
symbol = "SEK",
|
||||
icon = null,
|
||||
),
|
||||
timestamp = 0L,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.core.ui.components.transactions.state
|
|||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.ds.image.DeviceIconUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
|
|
@ -22,12 +23,13 @@ sealed interface TransactionItemUM {
|
|||
/**
|
||||
* Content state.
|
||||
*
|
||||
* @property amount signed numeric value, e.g. "+0.500913" / "-350.31"; no currency symbol embedded
|
||||
* @property amount signed numeric value, e.g. "+0.500913" / "-350.31"; no currency symbol embedded.
|
||||
* `null` hides the numeric value while [currencySymbol] still shows.
|
||||
* @property currencySymbol currency symbol shown alongside [amount], e.g. "BTC", "USDT"
|
||||
*/
|
||||
data class Content(
|
||||
override val txHash: String,
|
||||
val amount: String,
|
||||
val amount: String?,
|
||||
val currencySymbol: String,
|
||||
val time: String,
|
||||
val status: Status,
|
||||
|
|
@ -37,6 +39,7 @@ sealed interface TransactionItemUM {
|
|||
val title: TextReference,
|
||||
val subtitle: ContentSubtitle,
|
||||
val timestamp: Long,
|
||||
val warning: TextReference? = null,
|
||||
) : TransactionItemUM {
|
||||
|
||||
@Immutable
|
||||
|
|
@ -95,6 +98,19 @@ sealed interface TransactionItemUM {
|
|||
val deviceIconUM: DeviceIconUM,
|
||||
) : ContentSubtitle
|
||||
|
||||
/**
|
||||
* Counterparty asset ticker — renders as "to/from: <icon> <SYMBOL>". Used for express rows
|
||||
* (swap counterparty currency / onramp fiat), e.g. "to: ◎ POL" or "from: 🇸🇪 SEK".
|
||||
*
|
||||
* @property icon resolved counterparty currency icon, rendered via `CurrencyIcon`. `null` when no icon
|
||||
* is available (e.g. onramp fiat carries no `CryptoCurrency`) — the ticker then renders without a leading icon.
|
||||
*/
|
||||
data class Asset(
|
||||
val direction: Direction,
|
||||
val symbol: String,
|
||||
val icon: CurrencyIconState?,
|
||||
) : ContentSubtitle
|
||||
|
||||
enum class Direction { TO, FROM }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,13 +55,13 @@ internal class ExpressOnrampConverter : Converter<ExpressOnrampConverter.Input,
|
|||
payoutHash = entity.payoutHash,
|
||||
fromFiat = Amount(
|
||||
currencySymbol = entity.fromCurrencyCode,
|
||||
value = entity.fromAmount.toBigDecimalOrZero(),
|
||||
value = entity.fromAmount.toScaledBigDecimal(entity.fromPrecision),
|
||||
decimals = entity.fromPrecision,
|
||||
type = AmountType.FiatType(code = entity.fromCurrencyCode),
|
||||
),
|
||||
toAsset = ExpressTransactionAsset(
|
||||
id = ExpressAssetId(networkId = entity.to.network, contractAddress = entity.to.contractAddress),
|
||||
amount = (entity.to.actualAmount ?: entity.to.amount).toBigDecimalOrZero(),
|
||||
amount = (entity.to.actualAmount ?: entity.to.amount)?.toScaledBigDecimal(entity.to.decimals),
|
||||
decimals = entity.to.decimals,
|
||||
cryptoCurrency = value.toCurrency,
|
||||
),
|
||||
|
|
@ -90,13 +90,13 @@ private fun convertExchangeTransaction(value: ExpressSwapConverter.Input): Excha
|
|||
payoutHash = entity.payoutHash,
|
||||
fromAsset = ExpressTransactionAsset(
|
||||
id = ExpressAssetId(networkId = entity.from.network, contractAddress = entity.from.contractAddress),
|
||||
amount = entity.from.amount.toBigDecimalOrZero(),
|
||||
amount = entity.from.amount.toScaledBigDecimal(entity.from.decimals),
|
||||
decimals = entity.from.decimals,
|
||||
cryptoCurrency = value.fromCurrency,
|
||||
),
|
||||
toAsset = ExpressTransactionAsset(
|
||||
id = ExpressAssetId(networkId = entity.to.network, contractAddress = entity.to.contractAddress),
|
||||
amount = (entity.to.actualAmount ?: entity.to.amount).toBigDecimalOrZero(),
|
||||
amount = (entity.to.actualAmount ?: entity.to.amount).toScaledBigDecimal(entity.to.decimals),
|
||||
decimals = entity.to.decimals,
|
||||
cryptoCurrency = value.toCurrency,
|
||||
),
|
||||
|
|
@ -105,7 +105,12 @@ private fun convertExchangeTransaction(value: ExpressSwapConverter.Input): Excha
|
|||
|
||||
private fun parseIsoMillis(iso: String): Long = DateTime.parse(iso).millis
|
||||
|
||||
private fun String?.toBigDecimalOrZero(): BigDecimal = this?.toBigDecimalOrNull() ?: BigDecimal.ZERO
|
||||
/**
|
||||
* Parses a raw minimal-unit amount string from the express backend and scales it to the human-readable
|
||||
* value promised by [ExpressTransactionAsset.amount] (and the onramp fiat [Amount.value]).
|
||||
*/
|
||||
private fun String.toScaledBigDecimal(decimals: Int): BigDecimal =
|
||||
(this.toBigDecimalOrNull() ?: BigDecimal.ZERO).movePointLeft(decimals)
|
||||
|
||||
/**
|
||||
* Active (non-terminal) RAW status values passed to the DAO `observe…` queries so in-progress deals
|
||||
|
|
|
|||
|
|
@ -31,8 +31,9 @@ internal class ExpressTxHistoryConverterTest {
|
|||
assertThat(swap.txInfo).isNull()
|
||||
assertThat(swap.tx.status).isEqualTo(ExpressExchangeStatus.Waiting)
|
||||
assertThat(swap.createdAtMillis).isEqualTo(DateTime.parse(CREATED_AT).millis)
|
||||
assertThat(swap.tx.fromAsset.amount).isEqualTo(BigDecimal("1.5"))
|
||||
assertThat(swap.tx.toAsset.amount).isEqualTo(BigDecimal("0.001"))
|
||||
// Raw backend amounts are scaled by decimals into the human-readable value the domain model promises.
|
||||
assertThat(swap.tx.fromAsset.amount).isEquivalentAccordingToCompareTo(BigDecimal("1.5"))
|
||||
assertThat(swap.tx.toAsset.amount).isEquivalentAccordingToCompareTo(BigDecimal("0.001"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -50,14 +51,14 @@ internal class ExpressTxHistoryConverterTest {
|
|||
|
||||
@Test
|
||||
fun `GIVEN exchange entity with actual amount WHEN toOutgoingSwap THEN to-asset uses actual amount`() {
|
||||
// Arrange
|
||||
val entity = createExchangeEntity(toAmount = "0.001", toActualAmount = "0.00099")
|
||||
// Arrange (raw minimal-unit amounts, to-asset decimals = 8)
|
||||
val entity = createExchangeEntity(toAmount = "100000", toActualAmount = "99000")
|
||||
|
||||
// Act
|
||||
val swap = swapConverter.convert(ExpressSwapConverter.Input(entity, provider = null, isOutgoing = true))
|
||||
|
||||
// Assert
|
||||
assertThat(swap.tx.toAsset.amount).isEqualTo(BigDecimal("0.00099"))
|
||||
assertThat(swap.tx.toAsset.amount).isEquivalentAccordingToCompareTo(BigDecimal("0.00099"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -73,17 +74,18 @@ internal class ExpressTxHistoryConverterTest {
|
|||
assertThat(onramp.txInfo).isNull()
|
||||
assertThat(onramp.tx.status).isEqualTo(ExpressOnrampStatus.Finished)
|
||||
assertThat(onramp.tx.fromFiat.currencySymbol).isEqualTo("USD")
|
||||
assertThat(onramp.tx.fromFiat.value).isEqualTo(BigDecimal("100.0"))
|
||||
assertThat(onramp.tx.fromFiat.value).isEquivalentAccordingToCompareTo(BigDecimal("100"))
|
||||
assertThat(onramp.tx.fromFiat.decimals).isEqualTo(2)
|
||||
assertThat(onramp.tx.fromFiat.type).isEqualTo(AmountType.FiatType("USD"))
|
||||
assertThat(onramp.tx.toAsset.amount).isEqualTo(BigDecimal("0.5"))
|
||||
assertThat(onramp.tx.toAsset.amount).isEquivalentAccordingToCompareTo(BigDecimal("0.5"))
|
||||
}
|
||||
|
||||
private fun createExchangeEntity(
|
||||
payinHash: String? = "payin",
|
||||
payoutHash: String? = "payout",
|
||||
status: String = "waiting",
|
||||
toAmount: String = "0.001",
|
||||
// Raw minimal-unit amount (to-asset decimals = 8) → 0.001
|
||||
toAmount: String = "100000",
|
||||
toActualAmount: String? = null,
|
||||
) = ExpressExchangeEntity(
|
||||
txId = "tx-1",
|
||||
|
|
@ -111,7 +113,8 @@ internal class ExpressTxHistoryConverterTest {
|
|||
contractAddress = "",
|
||||
network = "ethereum",
|
||||
decimals = 18,
|
||||
amount = "1.5",
|
||||
// Raw minimal-unit amount (decimals = 18) → 1.5
|
||||
amount = "1500000000000000000",
|
||||
actualAmount = null,
|
||||
),
|
||||
to = ExpressExchangeEntity.AssetEmbedded(
|
||||
|
|
@ -139,13 +142,15 @@ internal class ExpressTxHistoryConverterTest {
|
|||
createdAt = CREATED_AT,
|
||||
updatedAt = CREATED_AT,
|
||||
fromCurrencyCode = "USD",
|
||||
fromAmount = "100.0",
|
||||
// Raw minimal-unit fiat amount (precision = 2) → 100
|
||||
fromAmount = "10000",
|
||||
fromPrecision = 2,
|
||||
to = ExpressOnrampEntity.AssetEmbedded(
|
||||
contractAddress = "0xtoken",
|
||||
network = "ethereum",
|
||||
decimals = 18,
|
||||
amount = "0.5",
|
||||
// Raw minimal-unit amount (decimals = 18) → 0.5
|
||||
amount = "500000000000000000",
|
||||
actualAmount = null,
|
||||
),
|
||||
paymentMethod = "card",
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ import java.math.BigDecimal
|
|||
* A crypto asset leg of an express operation: which asset and how much of it moved.
|
||||
*
|
||||
* @property id The asset identifier (network id + contract address).
|
||||
* @property amount Human-readable amount (already scaled by [decimals]).
|
||||
* @property amount Human-readable amount (already scaled by [decimals]); `null` when the backend provided no amount.
|
||||
* @property decimals The asset's decimals.
|
||||
* @property cryptoCurrency The portfolio [CryptoCurrency] this asset was resolved to (matched by network id +
|
||||
* contract address across all accounts). `null` when no portfolio currency matched and no fallback could be built.
|
||||
*/
|
||||
data class ExpressTransactionAsset(
|
||||
val id: ExpressAsset.ID,
|
||||
val amount: BigDecimal,
|
||||
val amount: BigDecimal?,
|
||||
val decimals: Int,
|
||||
val cryptoCurrency: CryptoCurrency? = null,
|
||||
)
|
||||
|
|
@ -14,6 +14,7 @@ import com.tangem.domain.models.wallet.UserWallet
|
|||
import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent
|
||||
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
|
||||
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
|
||||
import com.tangem.domain.txhistory.TxHistoryFeatureToggles
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTransactionsClickIntents
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotification
|
||||
|
|
@ -47,6 +48,7 @@ internal class ExpressStatusFactory @AssistedInject constructor(
|
|||
onrampStatusFactory: OnrampStatusFactory.Factory,
|
||||
exchangeStatusFactory: ExchangeStatusFactory.Factory,
|
||||
private val swapFeatureToggles: SwapFeatureToggles,
|
||||
private val txHistoryFeatureToggles: TxHistoryFeatureToggles,
|
||||
) {
|
||||
|
||||
private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||
|
|
@ -106,12 +108,17 @@ internal class ExpressStatusFactory @AssistedInject constructor(
|
|||
if (currentTx is ExchangeUM && currentTx.activeStatus == ExchangeStatus.Finished) {
|
||||
updateBalance(currentTx.toCryptoCurrency)
|
||||
}
|
||||
val expressTxsToDisplay = expressTxs.filterNot {
|
||||
when (it) {
|
||||
is ExpressTransactionStateUM.OnrampUM -> it.activeStatus.isHidden
|
||||
else -> false
|
||||
}
|
||||
}.toPersistentList()
|
||||
|
||||
val expressTxsToDisplay = if (txHistoryFeatureToggles.isNewTxHistoryEnabled) {
|
||||
persistentListOf()
|
||||
} else {
|
||||
expressTxs.filterNot {
|
||||
when (it) {
|
||||
is ExpressTransactionStateUM.OnrampUM -> it.activeStatus.isHidden
|
||||
else -> false
|
||||
}
|
||||
}.toPersistentList()
|
||||
}
|
||||
return state.copy(
|
||||
transactions = expressTxs,
|
||||
transactionsToDisplay = expressTxsToDisplay,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,191 @@
|
|||
package com.tangem.features.txhistory.converter
|
||||
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Direction as RowDirection
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Status
|
||||
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.ContentSubtitle
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.ContentSubtitle.Direction as SubtitleDirection
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.domain.express.models.ExpressExchangeStatus
|
||||
import com.tangem.domain.express.models.ExpressOnrampStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.txhistory.model.ExpressTx
|
||||
import com.tangem.features.txhistory.impl.R
|
||||
import com.tangem.features.txhistory.utils.TxHistoryUiActions
|
||||
import com.tangem.utils.StringsSigns
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Maps an [ExpressTx] (swap / onramp) row directly to [TransactionItemUM.Content].
|
||||
*
|
||||
* The viewed leg is [CryptoCurrency] ([currency], the token-details currency): outgoing swap shows the pay-in
|
||||
* (`from`) amount with a minus, incoming swap / onramp shows the received (`to`) amount with a plus. The 26 typed
|
||||
* express statuses collapse into the three [Status] buckets (those drive title/icon/amount colors in the row UI).
|
||||
*
|
||||
* The counterparty ticker symbol+icon come from the resolved [ExpressTransactionAsset.cryptoCurrency] (swap);
|
||||
* onramp shows the real fiat code with no icon yet (fiat carries no `CryptoCurrency`). The row click opens the
|
||||
* explorer.
|
||||
*/
|
||||
internal class ExpressTxToTransactionItemUMConverter(
|
||||
private val currency: CryptoCurrency,
|
||||
private val txHistoryUiActions: TxHistoryUiActions,
|
||||
) : Converter<ExpressTx, TransactionItemUM> {
|
||||
|
||||
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
|
||||
|
||||
override fun convert(value: ExpressTx): TransactionItemUM = when (value) {
|
||||
is ExpressTx.Swap -> swapContent(value)
|
||||
is ExpressTx.Onramp -> onrampContent(value)
|
||||
}
|
||||
|
||||
private fun swapContent(swap: ExpressTx.Swap): TransactionItemUM.Content {
|
||||
val status = swap.tx.status.toUiStatus()
|
||||
val viewedAmount = if (swap.isOutgoing) swap.tx.fromAsset.amount else swap.tx.toAsset.amount
|
||||
val counterparty = if (swap.isOutgoing) swap.tx.toAsset else swap.tx.fromAsset
|
||||
val prefix = when {
|
||||
status is Status.Failed -> ""
|
||||
swap.isOutgoing -> StringsSigns.MINUS
|
||||
else -> StringsSigns.PLUS
|
||||
}
|
||||
return buildContent(
|
||||
tx = swap,
|
||||
status = status,
|
||||
amount = formatAmount(viewedAmount, prefix),
|
||||
direction = if (swap.isOutgoing) RowDirection.OUTGOING else RowDirection.INCOMING,
|
||||
iconRes = R.drawable.ic_exchange_vertical_24,
|
||||
title = swapTitle(status),
|
||||
subtitle = ContentSubtitle.Asset(
|
||||
direction = if (swap.isOutgoing) SubtitleDirection.TO else SubtitleDirection.FROM,
|
||||
symbol = counterparty.cryptoCurrency?.symbol ?: counterparty.id.networkId,
|
||||
icon = counterparty.cryptoCurrency?.let(iconStateConverter::convert),
|
||||
),
|
||||
// TODO: replace null to warning logic.
|
||||
warning = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onrampContent(onramp: ExpressTx.Onramp): TransactionItemUM.Content {
|
||||
val status = onramp.tx.status.toUiStatus()
|
||||
val prefix = when {
|
||||
status is Status.Failed -> ""
|
||||
status is Status.Confirmed -> StringsSigns.PLUS
|
||||
else -> StringsSigns.TILDE_SIGN
|
||||
}
|
||||
return buildContent(
|
||||
tx = onramp,
|
||||
status = status,
|
||||
amount = formatAmount(onramp.tx.toAsset.amount, prefix),
|
||||
direction = RowDirection.INCOMING,
|
||||
iconRes = R.drawable.ic_tangem_card_24,
|
||||
title = onrampTitle(status),
|
||||
subtitle = ContentSubtitle.Asset(
|
||||
direction = SubtitleDirection.FROM,
|
||||
symbol = onramp.tx.fromFiat.currencySymbol,
|
||||
// TODO: fiat carries no OnrampCurrency, so no icon yet — render with a fiat country flag once available.
|
||||
icon = null,
|
||||
),
|
||||
// TODO: replace null to warning logic.
|
||||
warning = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun buildContent(
|
||||
tx: ExpressTx,
|
||||
status: Status,
|
||||
amount: String?,
|
||||
direction: RowDirection,
|
||||
iconRes: Int,
|
||||
title: TextReference,
|
||||
subtitle: ContentSubtitle,
|
||||
warning: TextReference?,
|
||||
): TransactionItemUM.Content {
|
||||
val explorerHash = tx.matchHash ?: tx.txId
|
||||
return TransactionItemUM.Content(
|
||||
txHash = explorerHash,
|
||||
amount = amount,
|
||||
currencySymbol = currency.symbol,
|
||||
time = tx.timestampMillis.toTimeFormat(),
|
||||
status = status,
|
||||
direction = direction,
|
||||
onClick = { txHistoryUiActions.openTxInExplorer(explorerHash) },
|
||||
iconRes = iconRes,
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
timestamp = tx.timestampMillis,
|
||||
warning = warning,
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatAmount(amount: BigDecimal?, prefix: String): String? =
|
||||
amount?.let { prefix + it.format { crypto(symbol = "", decimals = currency.decimals) }.trim() }
|
||||
|
||||
private fun swapTitle(status: Status): TextReference = when (status) {
|
||||
is Status.Confirmed -> resourceReference(R.string.common_swapped)
|
||||
is Status.Unconfirmed -> resourceReference(R.string.common_swapping)
|
||||
is Status.Failed ->
|
||||
resourceReference(R.string.common_action_failed, wrappedList(resourceReference(R.string.common_swapping)))
|
||||
}
|
||||
|
||||
private fun onrampTitle(status: Status): TextReference = when (status) {
|
||||
is Status.Confirmed -> resourceReference(R.string.tx_history_onramp_topped_up)
|
||||
is Status.Unconfirmed -> resourceReference(R.string.tx_history_onramp_top_up)
|
||||
is Status.Failed -> resourceReference(
|
||||
R.string.common_action_failed,
|
||||
wrappedList(resourceReference(R.string.tx_history_onramp_top_up)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Status mapping
|
||||
|
||||
/**
|
||||
* Collapses the typed swap status into a UI [Status] bucket: the single success state ([Finished][Confirmed]),
|
||||
* the failure/return states ([Failed]/[TxFailed]/[Refunded]/[Expired]/[Unknown]) → Failed, everything in flight
|
||||
* (incl. [Verifying] and [Paused]) → Unconfirmed.
|
||||
*/
|
||||
private fun ExpressExchangeStatus.toUiStatus(): Status = when (this) {
|
||||
ExpressExchangeStatus.Finished -> Status.Confirmed
|
||||
ExpressExchangeStatus.Failed,
|
||||
ExpressExchangeStatus.TxFailed,
|
||||
ExpressExchangeStatus.Refunded,
|
||||
ExpressExchangeStatus.Expired,
|
||||
ExpressExchangeStatus.Unknown,
|
||||
-> Status.Failed
|
||||
ExpressExchangeStatus.Preview,
|
||||
ExpressExchangeStatus.Created,
|
||||
ExpressExchangeStatus.ExchangeTxSent,
|
||||
ExpressExchangeStatus.Waiting,
|
||||
ExpressExchangeStatus.WaitingTxHash,
|
||||
ExpressExchangeStatus.Confirming,
|
||||
ExpressExchangeStatus.Exchanging,
|
||||
ExpressExchangeStatus.Sending,
|
||||
ExpressExchangeStatus.Verifying,
|
||||
ExpressExchangeStatus.Paused,
|
||||
-> Status.Unconfirmed
|
||||
}
|
||||
|
||||
private fun ExpressOnrampStatus.toUiStatus(): Status = when (this) {
|
||||
ExpressOnrampStatus.Finished -> Status.Confirmed
|
||||
ExpressOnrampStatus.Failed,
|
||||
ExpressOnrampStatus.Expired,
|
||||
ExpressOnrampStatus.Unknown,
|
||||
-> Status.Failed
|
||||
ExpressOnrampStatus.Created,
|
||||
ExpressOnrampStatus.WaitingForPayment,
|
||||
ExpressOnrampStatus.PaymentProcessing,
|
||||
ExpressOnrampStatus.Verifying,
|
||||
ExpressOnrampStatus.Paid,
|
||||
ExpressOnrampStatus.Sending,
|
||||
ExpressOnrampStatus.Paused,
|
||||
-> Status.Unconfirmed
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
@ -4,21 +4,20 @@ import com.tangem.core.ui.components.transactions.state.TransactionItemUM
|
|||
import com.tangem.domain.txhistory.model.ExpressTx
|
||||
import com.tangem.domain.txhistory.model.OnChainTx
|
||||
import com.tangem.domain.txhistory.model.TxHistoryInfo
|
||||
import com.tangem.features.txhistory.utils.toSyntheticTxInfo
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converts a merged [TxHistoryInfo] row to [TransactionItemUM], delegating to the on-chain
|
||||
* [TxHistoryItemToTransactionItemUMConverter]: on-chain rows convert their `TxInfo` directly, express
|
||||
* rows convert a synthesized `TxInfo` view (see [toSyntheticTxInfo]).
|
||||
* Converts a merged [TxHistoryInfo] row to [TransactionItemUM]: on-chain rows convert their `TxInfo` via
|
||||
* [TxHistoryItemToTransactionItemUMConverter]; express rows map directly via [ExpressTxToTransactionItemUMConverter].
|
||||
*/
|
||||
internal class TxHistoryInfoToTransactionItemUMConverter(
|
||||
private val txInfoConverter: TxHistoryItemToTransactionItemUMConverter,
|
||||
private val expressConverter: ExpressTxToTransactionItemUMConverter,
|
||||
) : Converter<TxHistoryInfo, TransactionItemUM> {
|
||||
|
||||
override fun convert(value: TxHistoryInfo): TransactionItemUM = when (value) {
|
||||
is OnChainTx -> convertOnChain(value)
|
||||
is ExpressTx -> txInfoConverter.convert(value.toSyntheticTxInfo())
|
||||
is ExpressTx -> expressConverter.convert(value)
|
||||
}
|
||||
|
||||
private fun convertOnChain(value: OnChainTx): TransactionItemUM = when (value) {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
|
|||
import com.tangem.domain.wallets.usecase.GetWalletIconUseCase
|
||||
import com.tangem.domain.txhistory.TxHistoryFeatureToggles
|
||||
import com.tangem.features.txhistory.component.TxHistoryComponent
|
||||
import com.tangem.features.txhistory.converter.ExpressTxToTransactionItemUMConverter
|
||||
import com.tangem.features.txhistory.converter.TxHistoryInfoToTransactionItemUMConverter
|
||||
import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionItemUMConverter
|
||||
import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionStateConverter
|
||||
|
|
@ -39,6 +40,7 @@ import com.tangem.features.txhistory.utils.HistoryTxListManager
|
|||
import com.tangem.features.txhistory.utils.TxHistoryListManager
|
||||
import com.tangem.features.txhistory.utils.TxHistoryUiActions
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
import com.tangem.utils.annotations.RemoveWithToggle
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -100,9 +102,11 @@ internal class TxHistoryModel @Inject constructor(
|
|||
emptyFlow()
|
||||
}
|
||||
|
||||
@RemoveWithToggle("APP_REDESIGN_ENABLED")
|
||||
private val legacyTxHistoryItemConverter =
|
||||
TxHistoryItemToTransactionStateConverter(currency = params.currency, txHistoryUiActions = this)
|
||||
|
||||
@RemoveWithToggle("AND_15767_NEW_TX_HISTORY_ENABLED")
|
||||
private val txHistoryListManager: TxHistoryListManager? = if (!txHistoryFeatureToggle.isNewTxHistoryEnabled) {
|
||||
TxHistoryListManager(
|
||||
repository = repository,
|
||||
|
|
@ -193,7 +197,6 @@ internal class TxHistoryModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// Temporary: express rows are mapped to UI via a synthesized TxInfo (see ExpressTx.toSyntheticTxInfo).
|
||||
private fun buildUiItems(
|
||||
merged: List<TxHistoryInfo>,
|
||||
lookup: TxHistoryLookupContext,
|
||||
|
|
@ -204,6 +207,10 @@ internal class TxHistoryModel @Inject constructor(
|
|||
txHistoryUiActions = this,
|
||||
lookupContext = lookup,
|
||||
),
|
||||
expressConverter = ExpressTxToTransactionItemUMConverter(
|
||||
currency = params.currency,
|
||||
txHistoryUiActions = this,
|
||||
),
|
||||
)
|
||||
|
||||
val items = mutableListOf<TxHistoryItemsUM.TxHistoryItemUM>()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
package com.tangem.features.txhistory.utils
|
||||
|
||||
import com.tangem.domain.express.models.ExpressExchangeStatus
|
||||
import com.tangem.domain.express.models.ExpressOnrampStatus
|
||||
import com.tangem.domain.models.network.TxInfo
|
||||
import com.tangem.domain.txhistory.model.ExpressTx
|
||||
import com.tangem.domain.txhistory.model.OnChainTx
|
||||
|
|
@ -50,48 +48,4 @@ private fun ExpressTx.withMatchedTxInfo(txInfo: TxInfo): ExpressTx {
|
|||
is ExpressTx.Swap -> copy(txInfo = matched)
|
||||
is ExpressTx.Onramp -> copy(txInfo = matched)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synthesizes a [TxInfo] view of an express op so it can be rendered by the existing
|
||||
* [com.tangem.features.txhistory.converter.TxHistoryItemToTransactionItemUMConverter]. Rendered as a
|
||||
* [TxInfo.TransactionType.Swap] for now (onramp included). The amount is the viewed-currency leg.
|
||||
*/
|
||||
internal fun ExpressTx.toSyntheticTxInfo(): TxInfo {
|
||||
val viewedAmount = when (this) {
|
||||
is ExpressTx.Swap -> if (isOutgoing) tx.fromAsset.amount else tx.toAsset.amount
|
||||
is ExpressTx.Onramp -> tx.toAsset.amount
|
||||
}
|
||||
val isOutgoing = when (this) {
|
||||
is ExpressTx.Swap -> this.isOutgoing
|
||||
is ExpressTx.Onramp -> false
|
||||
}
|
||||
return TxInfo(
|
||||
// matchHash is the on-chain hash (== the matched leg's hash, enables the explorer link); else txId.
|
||||
txHash = matchHash ?: txId,
|
||||
timestampInMillis = timestampMillis,
|
||||
isOutgoing = isOutgoing,
|
||||
destinationType = TxInfo.DestinationType.Single(TxInfo.AddressType.User(address = "")),
|
||||
sourceType = TxInfo.SourceType.Single(address = ""),
|
||||
interactionAddressType = null,
|
||||
status = toTransactionStatus(),
|
||||
type = TxInfo.TransactionType.Swap,
|
||||
amount = viewedAmount,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the typed express status to the on-chain-shaped [TxInfo.TransactionStatus] used by the UI:
|
||||
* the single success state (`Finished`) → Confirmed, any other terminal state → Failed, in-progress → Unconfirmed.
|
||||
*/
|
||||
private fun ExpressTx.toTransactionStatus(): TxInfo.TransactionStatus {
|
||||
val isFinished = when (this) {
|
||||
is ExpressTx.Swap -> tx.status == ExpressExchangeStatus.Finished
|
||||
is ExpressTx.Onramp -> tx.status == ExpressOnrampStatus.Finished
|
||||
}
|
||||
return when {
|
||||
isFinished -> TxInfo.TransactionStatus.Confirmed
|
||||
isTerminal -> TxInfo.TransactionStatus.Failed
|
||||
else -> TxInfo.TransactionStatus.Unconfirmed
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
package com.tangem.features.txhistory.converter
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.google.common.truth.Truth.assertWithMessage
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Status
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.ContentSubtitle
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.express.models.ExchangeTransaction
|
||||
import com.tangem.domain.express.models.ExpressAsset.ID as ExpressAssetId
|
||||
import com.tangem.domain.express.models.ExpressExchangeStatus
|
||||
import com.tangem.domain.express.models.ExpressOnrampStatus
|
||||
import com.tangem.domain.express.models.ExpressTransactionAsset
|
||||
import com.tangem.domain.express.models.OnrampTransaction
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.tokens.model.Amount
|
||||
import com.tangem.domain.tokens.model.AmountType
|
||||
import com.tangem.domain.txhistory.model.ExpressTx
|
||||
import com.tangem.features.txhistory.impl.R
|
||||
import com.tangem.features.txhistory.utils.TxHistoryUiActions
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class ExpressTxToTransactionItemUMConverterTest {
|
||||
|
||||
private val txHistoryUiActions: TxHistoryUiActions = mockk(relaxed = true)
|
||||
private val coin: CryptoCurrency.Coin = createCoin(symbol = "ETH", decimals = 18)
|
||||
|
||||
private val converter = ExpressTxToTransactionItemUMConverter(
|
||||
currency = coin,
|
||||
txHistoryUiActions = txHistoryUiActions,
|
||||
)
|
||||
|
||||
// region Status → bucket
|
||||
|
||||
@Test
|
||||
fun `GIVEN every swap status WHEN convert THEN mapped to expected status bucket`() {
|
||||
val cases = mapOf(
|
||||
ExpressExchangeStatus.Finished to Status.Confirmed,
|
||||
ExpressExchangeStatus.Failed to Status.Failed,
|
||||
ExpressExchangeStatus.TxFailed to Status.Failed,
|
||||
ExpressExchangeStatus.Refunded to Status.Failed,
|
||||
ExpressExchangeStatus.Expired to Status.Failed,
|
||||
ExpressExchangeStatus.Unknown to Status.Failed,
|
||||
ExpressExchangeStatus.Preview to Status.Unconfirmed,
|
||||
ExpressExchangeStatus.Created to Status.Unconfirmed,
|
||||
ExpressExchangeStatus.ExchangeTxSent to Status.Unconfirmed,
|
||||
ExpressExchangeStatus.Waiting to Status.Unconfirmed,
|
||||
ExpressExchangeStatus.WaitingTxHash to Status.Unconfirmed,
|
||||
ExpressExchangeStatus.Confirming to Status.Unconfirmed,
|
||||
ExpressExchangeStatus.Exchanging to Status.Unconfirmed,
|
||||
ExpressExchangeStatus.Sending to Status.Unconfirmed,
|
||||
ExpressExchangeStatus.Verifying to Status.Unconfirmed,
|
||||
ExpressExchangeStatus.Paused to Status.Unconfirmed,
|
||||
)
|
||||
// every enum entry is covered (guards against new statuses silently falling through)
|
||||
assertThat(cases.keys).containsExactlyElementsIn(ExpressExchangeStatus.entries)
|
||||
|
||||
cases.forEach { (status, expected) ->
|
||||
val result = converter.convert(createSwap(status = status)) as TransactionItemUM.Content
|
||||
assertWithMessage(status.name).that(result.status).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN every onramp status WHEN convert THEN mapped to expected status bucket`() {
|
||||
val cases = mapOf(
|
||||
ExpressOnrampStatus.Finished to Status.Confirmed,
|
||||
ExpressOnrampStatus.Failed to Status.Failed,
|
||||
ExpressOnrampStatus.Expired to Status.Failed,
|
||||
ExpressOnrampStatus.Unknown to Status.Failed,
|
||||
ExpressOnrampStatus.Created to Status.Unconfirmed,
|
||||
ExpressOnrampStatus.WaitingForPayment to Status.Unconfirmed,
|
||||
ExpressOnrampStatus.PaymentProcessing to Status.Unconfirmed,
|
||||
ExpressOnrampStatus.Verifying to Status.Unconfirmed,
|
||||
ExpressOnrampStatus.Paid to Status.Unconfirmed,
|
||||
ExpressOnrampStatus.Sending to Status.Unconfirmed,
|
||||
ExpressOnrampStatus.Paused to Status.Unconfirmed,
|
||||
)
|
||||
assertThat(cases.keys).containsExactlyElementsIn(ExpressOnrampStatus.entries)
|
||||
|
||||
cases.forEach { (status, expected) ->
|
||||
val result = converter.convert(createOnramp(status = status)) as TransactionItemUM.Content
|
||||
assertWithMessage(status.name).that(result.status).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Amount sign / prefix
|
||||
|
||||
@Test
|
||||
fun `GIVEN outgoing swap WHEN convert THEN amount is negative from-leg`() {
|
||||
val result = converter.convert(
|
||||
createSwap(status = ExpressExchangeStatus.Waiting, isOutgoing = true),
|
||||
) as TransactionItemUM.Content
|
||||
|
||||
assertThat(result.direction).isEqualTo(TransactionItemUM.Content.Direction.OUTGOING)
|
||||
assertThat(result.amount).startsWith("-")
|
||||
assertThat(result.amount).contains("1.5")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN incoming swap WHEN convert THEN amount is positive to-leg`() {
|
||||
val result = converter.convert(
|
||||
createSwap(status = ExpressExchangeStatus.Waiting, isOutgoing = false),
|
||||
) as TransactionItemUM.Content
|
||||
|
||||
assertThat(result.direction).isEqualTo(TransactionItemUM.Content.Direction.INCOMING)
|
||||
assertThat(result.amount).startsWith("+")
|
||||
assertThat(result.amount).contains("0.001")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN finished onramp WHEN convert THEN amount prefixed with plus`() {
|
||||
val result = converter.convert(createOnramp(status = ExpressOnrampStatus.Finished)) as TransactionItemUM.Content
|
||||
assertThat(result.amount).startsWith("+")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN in-progress onramp WHEN convert THEN amount prefixed with tilde`() {
|
||||
val result = converter.convert(createOnramp(status = ExpressOnrampStatus.Sending)) as TransactionItemUM.Content
|
||||
assertThat(result.amount).startsWith("~")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN failed onramp WHEN convert THEN amount has no sign prefix`() {
|
||||
val result = converter.convert(createOnramp(status = ExpressOnrampStatus.Failed)) as TransactionItemUM.Content
|
||||
assertThat(requireNotNull(result.amount).first())
|
||||
.isIn(listOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN swap with null viewed amount WHEN convert THEN amount is null`() {
|
||||
val result = converter.convert(
|
||||
createSwap(status = ExpressExchangeStatus.Waiting, isOutgoing = true, fromAmount = null),
|
||||
) as TransactionItemUM.Content
|
||||
|
||||
assertThat(result.amount).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN onramp with null amount WHEN convert THEN amount is null`() {
|
||||
val result = converter.convert(
|
||||
createOnramp(status = ExpressOnrampStatus.Sending, toAmount = null),
|
||||
) as TransactionItemUM.Content
|
||||
|
||||
assertThat(result.amount).isNull()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Title / subtitle / warning / click
|
||||
|
||||
@Test
|
||||
fun `GIVEN swap statuses WHEN convert THEN status-aware title`() {
|
||||
val swapping = converter.convert(createSwap(status = ExpressExchangeStatus.Waiting)) as TransactionItemUM.Content
|
||||
val swapped = converter.convert(createSwap(status = ExpressExchangeStatus.Finished)) as TransactionItemUM.Content
|
||||
|
||||
assertThat(swapping.title).isEqualTo(resourceReference(R.string.common_swapping))
|
||||
assertThat(swapped.title).isEqualTo(resourceReference(R.string.common_swapped))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN onramp statuses WHEN convert THEN status-aware title`() {
|
||||
val topUp = converter.convert(createOnramp(status = ExpressOnrampStatus.Sending)) as TransactionItemUM.Content
|
||||
val toppedUp = converter.convert(createOnramp(status = ExpressOnrampStatus.Finished)) as TransactionItemUM.Content
|
||||
|
||||
assertThat(topUp.title).isEqualTo(resourceReference(R.string.tx_history_onramp_top_up))
|
||||
assertThat(toppedUp.title).isEqualTo(resourceReference(R.string.tx_history_onramp_topped_up))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN outgoing swap WHEN convert THEN subtitle shows TO counterparty ticker`() {
|
||||
val result = converter.convert(
|
||||
createSwap(status = ExpressExchangeStatus.Waiting, isOutgoing = true),
|
||||
) as TransactionItemUM.Content
|
||||
|
||||
val subtitle = result.subtitle as ContentSubtitle.Asset
|
||||
assertThat(subtitle.direction).isEqualTo(ContentSubtitle.Direction.TO)
|
||||
assertThat(subtitle.symbol).isEqualTo("btc") // mock: counterparty (to-leg) networkId
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN onramp WHEN convert THEN subtitle shows FROM fiat code`() {
|
||||
val result = converter.convert(createOnramp(status = ExpressOnrampStatus.Sending)) as TransactionItemUM.Content
|
||||
|
||||
val subtitle = result.subtitle as ContentSubtitle.Asset
|
||||
assertThat(subtitle.direction).isEqualTo(ContentSubtitle.Direction.FROM)
|
||||
assertThat(subtitle.symbol).isEqualTo("SEK")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN matched on-chain leg WHEN row clicked THEN opens explorer by match hash`() {
|
||||
val result = converter.convert(
|
||||
createSwap(status = ExpressExchangeStatus.Waiting, matchHash = "0xhash", isOutgoing = true),
|
||||
) as TransactionItemUM.Content
|
||||
|
||||
result.onClick()
|
||||
|
||||
verify { txHistoryUiActions.openTxInExplorer("0xhash") }
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
private fun createSwap(
|
||||
status: ExpressExchangeStatus,
|
||||
matchHash: String? = null,
|
||||
isOutgoing: Boolean = true,
|
||||
fromAmount: BigDecimal? = BigDecimal("1.5"),
|
||||
toAmount: BigDecimal? = BigDecimal("0.001"),
|
||||
) = ExpressTx.Swap(
|
||||
tx = ExchangeTransaction(
|
||||
txId = "tx-1",
|
||||
status = status,
|
||||
createdAtMillis = 100,
|
||||
provider = null,
|
||||
payinHash = matchHash.takeIf { isOutgoing },
|
||||
payoutHash = matchHash.takeUnless { isOutgoing },
|
||||
fromAsset = ExpressTransactionAsset(
|
||||
id = ExpressAssetId(networkId = "eth", contractAddress = "0"),
|
||||
amount = fromAmount,
|
||||
decimals = 18,
|
||||
),
|
||||
toAsset = ExpressTransactionAsset(
|
||||
id = ExpressAssetId(networkId = "btc", contractAddress = "0xt"),
|
||||
amount = toAmount,
|
||||
decimals = 8,
|
||||
),
|
||||
),
|
||||
isOutgoing = isOutgoing,
|
||||
txInfo = null,
|
||||
)
|
||||
|
||||
private fun createOnramp(
|
||||
status: ExpressOnrampStatus,
|
||||
toAmount: BigDecimal? = BigDecimal("0.006339"),
|
||||
) = ExpressTx.Onramp(
|
||||
tx = OnrampTransaction(
|
||||
txId = "tx-2",
|
||||
status = status,
|
||||
createdAtMillis = 100,
|
||||
provider = null,
|
||||
payoutHash = null,
|
||||
fromFiat = Amount(
|
||||
currencySymbol = "SEK",
|
||||
value = BigDecimal("100"),
|
||||
decimals = 2,
|
||||
type = AmountType.FiatType(code = "SEK"),
|
||||
),
|
||||
toAsset = ExpressTransactionAsset(
|
||||
id = ExpressAssetId(networkId = "btc", contractAddress = "0"),
|
||||
amount = toAmount,
|
||||
decimals = 8,
|
||||
),
|
||||
),
|
||||
txInfo = null,
|
||||
)
|
||||
|
||||
private fun createCoin(symbol: String, decimals: Int): CryptoCurrency.Coin = CryptoCurrency.Coin(
|
||||
id = CryptoCurrency.ID(
|
||||
prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX,
|
||||
body = CryptoCurrency.ID.Body.NetworkId(rawId = "ethereum"),
|
||||
suffix = CryptoCurrency.ID.Suffix.RawID(rawId = "ethereum"),
|
||||
),
|
||||
network = Network(
|
||||
id = Network.ID(value = "ethereum", derivationPath = Network.DerivationPath.None),
|
||||
name = "Ethereum",
|
||||
currencySymbol = symbol,
|
||||
derivationPath = Network.DerivationPath.None,
|
||||
isTestnet = false,
|
||||
standardType = Network.StandardType.ERC20,
|
||||
hasFiatFeeRate = true,
|
||||
canHandleTokens = true,
|
||||
transactionExtrasType = Network.TransactionExtrasType.NONE,
|
||||
nameResolvingType = Network.NameResolvingType.NONE,
|
||||
),
|
||||
name = "Ethereum",
|
||||
symbol = symbol,
|
||||
decimals = decimals,
|
||||
iconUrl = null,
|
||||
isCustom = false,
|
||||
)
|
||||
}
|
||||
|
|
@ -161,8 +161,8 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
|
|||
assertThat(result.subtitle).isEqualTo(
|
||||
ContentSubtitle.Plain(resRef(R.string.transaction_history_earned_from_stake)),
|
||||
)
|
||||
assertThat(result.amount.startsWith(StringsSigns.PLUS)).isFalse()
|
||||
assertThat(result.amount.startsWith(StringsSigns.MINUS)).isFalse()
|
||||
assertThat(result.amount!!.startsWith(StringsSigns.PLUS)).isFalse()
|
||||
assertThat(result.amount!!.startsWith(StringsSigns.MINUS)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -514,7 +514,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
|
|||
|
||||
val result = coinConverter.convert(tx) as TransactionItemUM.Content
|
||||
|
||||
assertThat(result.amount.startsWith(StringsSigns.MINUS)).isTrue()
|
||||
assertThat(result.amount!!.startsWith(StringsSigns.MINUS)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -528,7 +528,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
|
|||
|
||||
val result = coinConverter.convert(tx) as TransactionItemUM.Content
|
||||
|
||||
assertThat(result.amount.startsWith(StringsSigns.PLUS)).isTrue()
|
||||
assertThat(result.amount!!.startsWith(StringsSigns.PLUS)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -543,8 +543,8 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
|
|||
|
||||
val result = coinConverter.convert(tx) as TransactionItemUM.Content
|
||||
|
||||
assertThat(result.amount.startsWith(StringsSigns.MINUS)).isFalse()
|
||||
assertThat(result.amount.startsWith(StringsSigns.PLUS)).isFalse()
|
||||
assertThat(result.amount!!.startsWith(StringsSigns.MINUS)).isFalse()
|
||||
assertThat(result.amount!!.startsWith(StringsSigns.PLUS)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -558,8 +558,8 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
|
|||
|
||||
val result = coinConverter.convert(tx) as TransactionItemUM.Content
|
||||
|
||||
assertThat(result.amount.startsWith(StringsSigns.MINUS)).isFalse()
|
||||
assertThat(result.amount.startsWith(StringsSigns.PLUS)).isFalse()
|
||||
assertThat(result.amount!!.startsWith(StringsSigns.MINUS)).isFalse()
|
||||
assertThat(result.amount!!.startsWith(StringsSigns.PLUS)).isFalse()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
|
|||
|
|
@ -85,21 +85,6 @@ internal class TxHistoryInfoMergerTest {
|
|||
assertThat(result.map { it.timestampMillis }).containsExactly(200L, 100L).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN outgoing swap WHEN toSyntheticTxInfo THEN viewed from-leg amount and swap type`() {
|
||||
// Arrange
|
||||
val swap = createSwap(matchHash = "missing", status = ExpressExchangeStatus.Waiting, isOutgoing = true)
|
||||
|
||||
// Act
|
||||
val txInfo = swap.toSyntheticTxInfo()
|
||||
|
||||
// Assert
|
||||
assertThat(txInfo.isOutgoing).isTrue()
|
||||
assertThat(txInfo.amount).isEqualTo(BigDecimal("1.5"))
|
||||
assertThat(txInfo.type).isEqualTo(TxInfo.TransactionType.Swap)
|
||||
assertThat(txInfo.status).isEqualTo(TxInfo.TransactionStatus.Unconfirmed)
|
||||
}
|
||||
|
||||
private fun createTxInfo(txHash: String, timestamp: Long) = TxInfo(
|
||||
txHash = txHash,
|
||||
timestampInMillis = timestamp,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue