From 30788b2c52de9a59975464155ebf3c821cfbce8e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 3 Jul 2026 13:34:43 +0300 Subject: [PATCH] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 2 + .../OnChainTxToDetailsUMConverter.kt | 50 ++++++- .../txhistory/entity/TxHistoryDetailsUM.kt | 39 ++++- .../ui/TxHistoryDetailsAmountBlock.kt | 133 +++++++++++++++--- ...TxHistoryDetailsModalBottomSheetContent.kt | 12 +- .../OnChainTxToDetailsUMConverterTest.kt | 82 ++++++++++- 6 files changed, 279 insertions(+), 39 deletions(-) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b54a4ae79a..f0a128eed2 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -2688,6 +2688,8 @@ %1$s withdrawn from Aave Yield Mode initialized Yield Mode reactivated + Returned + Supplied Supply to Aave %1$s supplied to Aave Withdraw from Aave diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/OnChainTxToDetailsUMConverter.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/OnChainTxToDetailsUMConverter.kt index 4b4d4f1c14..708a11ce0a 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/OnChainTxToDetailsUMConverter.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/OnChainTxToDetailsUMConverter.kt @@ -27,6 +27,9 @@ import com.tangem.utils.toBriefAddressFormat import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +/** Website the yield-supply protocol row links to — the hard-wired Aave integration's home page. */ +private const val AAVE_WEBSITE = "https://aave.com/" + /** * Converts an on-chain [TxInfo] to the [TxHistoryDetailsUM.SingleAsset] details card (Receive / Send / Transfer / * staking / yield-supply). A two-asset swap always surfaces as an [com.tangem.domain.txhistory.model.ExpressTx.Swap] @@ -62,14 +65,17 @@ internal class OnChainTxToDetailsUMConverter( /** * Protocol row of a yield-supply tx: the DeFi protocol the funds are supplied to. The yield-supply product is a * single hard-wired integration across the app (Aave — the [yield_module_provider][R.string.yield_module_provider] - * name), so the value is that constant provider rather than a per-tx resolved name. Mutually exclusive with - * [validatorRow] — a tx is either staking or yield-supply, never both. + * name), so the value is that constant provider rather than a per-tx resolved name, and the tap opens the + * constant [AAVE_WEBSITE]. Mutually exclusive with [validatorRow] — a tx is either staking or yield-supply, + * never both. */ private fun TxInfo.protocolRow(): TxHistoryDetailsUM.InfoRowUM? { if (type !is TransactionType.YieldSupply) return null return TxHistoryDetailsUM.InfoRowUM( label = resourceReference(R.string.staking_validator), value = resourceReference(R.string.yield_module_provider), + trailingIconRes = R.drawable.ic_arrow_top_right_24, + onClick = { onOpenValidator(AAVE_WEBSITE) }, ) } @@ -125,11 +131,37 @@ internal class OnChainTxToDetailsUMConverter( } private fun TxInfo.toAmountBlockUM(): TxHistoryDetailsUM.AmountBlockUM = TxHistoryDetailsUM.AmountBlockUM( - currencyIcon = iconStateConverter.convert(currency), + icon = amountIcon(), amount = stringReference(signedAmount(currency)), + label = amountLabel(), isFailed = status is TxInfo.TransactionStatus.Failed, ) + /** + * Amount icon. Yield-supply enter/exit shows the asset paired with the hard-wired Aave protocol icon, ordered by + * direction (Aave leads on "Supplied"/enter, the asset leads on "Returned"/exit — mirroring the two states in the + * design); every other type shows the single token avatar. Only enter/exit are paired for now — the remaining + * yield-supply variants (topup / withdraw) can adopt the same pair later. + */ + private fun TxInfo.amountIcon(): TxHistoryDetailsUM.AmountIconUM { + val asset = TxHistoryDetailsUM.AmountIconUM.Item.Currency(iconStateConverter.convert(currency)) + val aave = TxHistoryDetailsUM.AmountIconUM.Item.Resource(R.drawable.img_aave_22) + return when (type) { + is TransactionType.YieldSupply.Enter -> + TxHistoryDetailsUM.AmountIconUM.OverlappingPair(leading = aave, trailing = asset) + is TransactionType.YieldSupply.Exit -> + TxHistoryDetailsUM.AmountIconUM.OverlappingPair(leading = asset, trailing = aave) + else -> TxHistoryDetailsUM.AmountIconUM.Single(iconStateConverter.convert(currency)) + } + } + + /** "Supplied"/"Returned" label above the amount for yield-supply enter/exit; `null` (no label) for other types. */ + private fun TxInfo.amountLabel(): TextReference? = when (type) { + is TransactionType.YieldSupply.Enter -> resourceReference(R.string.yield_module_transaction_supplied) + is TransactionType.YieldSupply.Exit -> resourceReference(R.string.yield_module_transaction_returned) + else -> null + } + /** * Counterparty card ("Recipient" / "From"). Only the external-address avatar is produced — built from the `User` * interaction address (the same source the history list uses for its external-address subtitle); a counterparty that @@ -140,6 +172,12 @@ internal class OnChainTxToDetailsUMConverter( * now — a follow-up. */ private fun TxInfo.toCounterpartyUM(): TxHistoryDetailsUM.CounterpartyUM? { + // Contract interactions (yield-supply / staking / approve) talk to a protocol/validator, not a real recipient — + // no copyable counterparty card. + val isContractInteraction = type is TransactionType.YieldSupply || + type is TransactionType.Staking || + type is TransactionType.Approve + if (isContractInteraction) return null val address = (interactionAddressType as? TxInfo.InteractionAddressType.User)?.address ?: return null return TxHistoryDetailsUM.CounterpartyUM( label = counterpartyLabel(), @@ -158,13 +196,15 @@ internal class OnChainTxToDetailsUMConverter( /** * Signed crypto amount with inline symbol, e.g. `+ 350.31 USDT` / `- 350.31 USDT`. The sign is `-` for outgoing, `+` - * otherwise, and is dropped for zero amounts and for the failed state (a failed tx moved nothing) — the UI then only - * strikes the amount through and dims it via [TxHistoryDetailsUM.AmountBlockUM.isFailed]. + * otherwise, and is dropped for zero amounts, for the failed state (a failed tx moved nothing) and for yield-supply + * enter/exit (which reads "Supplied"/"Returned" via the label instead of a signed transfer) — the UI then only strikes + * the amount through and dims it via [TxHistoryDetailsUM.AmountBlockUM.isFailed]. */ private fun TxInfo.signedAmount(currency: CryptoCurrency): String { val formatted = amount.format { crypto(cryptoCurrency = currency, ignoreSymbolPosition = true) } val prefix = when { status is TxInfo.TransactionStatus.Failed -> "" + type is TransactionType.YieldSupply.Enter || type is TransactionType.YieldSupply.Exit -> "" amount.isZero() -> "" isOutgoing -> "${StringsSigns.MINUS} " else -> "${StringsSigns.PLUS} " diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/entity/TxHistoryDetailsUM.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/entity/TxHistoryDetailsUM.kt index f158ee4ea3..dabb48ccb8 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/entity/TxHistoryDetailsUM.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/entity/TxHistoryDetailsUM.kt @@ -137,22 +137,51 @@ internal sealed interface TxHistoryDetailsUM : TangemBottomSheetConfigContent { } /** - * Centered amount block of the single-asset card: token avatar (with network badge), the big signed crypto - * [amount] and the secondary [fiatAmount]. + * Centered amount block of the single-asset card: the [icon] (a single token avatar, or a yield-supply asset+Aave + * pair), an optional [label] above the amount ("Supplied" / "Returned" for yield supply), the big [amount] and the + * secondary [fiatAmount]. * - * [fiatAmount] is `null` while no fiat value is available (`TxInfo` has no fiat field yet) — the fiat line is then - * omitted entirely rather than shown as a placeholder. + * [label] is `null` for the plain single-asset types (Send / Receive / Transfer / staking) — the line is then + * omitted. [fiatAmount] is `null` while no fiat value is available (`TxInfo` has no fiat field yet) — the fiat line + * is then omitted entirely rather than shown as a placeholder. * * [isFailed] drives the failed visual state — the amount is struck through, recolored to tertiary and carries no * `+`/`−` sign (mirrors the status-driven recolor in the shared header). */ data class AmountBlockUM( - val currencyIcon: CurrencyIconState, + val icon: AmountIconUM, val amount: TextReference, + val label: TextReference? = null, val fiatAmount: TextReference? = null, val isFailed: Boolean, ) + /** Icon shown above the amount of the single-asset card. */ + @Immutable + sealed interface AmountIconUM { + + /** A single token avatar — Send / Receive / Transfer / staking. */ + data class Single(val currencyIcon: CurrencyIconState) : AmountIconUM + + /** + * Two overlapping avatars: [leading] is drawn on top (left, with a background-colored ring), [trailing] behind + * it (right). Used by yield supply — the asset and the hard-wired Aave protocol icon — with the order set by the + * transaction direction: Aave leads on "Supplied" (enter), the asset leads on "Returned" (exit). + */ + data class OverlappingPair(val leading: Item, val trailing: Item) : AmountIconUM + + /** One avatar of an [OverlappingPair]. */ + @Immutable + sealed interface Item { + + /** A token avatar backed by its [state]. */ + data class Currency(val state: CurrencyIconState) : Item + + /** A hard-wired drawable (e.g. the Aave protocol icon). */ + data class Resource(@DrawableRes val resId: Int) : Item + } + } + /** * A single info row of the details card: a [label] on the leading side and its [value] on the trailing side * (e.g. `Network fee` → `0.00056 ETH`, `Rate` → `1 POL ≈ 0.36 USDT`). Rendered by [TxHistoryDetailsInfoRows]. diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/ui/TxHistoryDetailsAmountBlock.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/ui/TxHistoryDetailsAmountBlock.kt index e5c7804469..2c81c2d3bf 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/ui/TxHistoryDetailsAmountBlock.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/ui/TxHistoryDetailsAmountBlock.kt @@ -1,15 +1,22 @@ package com.tangem.features.txhistory.ui import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text 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.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview @@ -25,13 +32,18 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.features.txhistory.entity.TxHistoryDetailsUM +private val IconSize = 72.dp +private val PairOverlap = 20.dp +private val PairRing = 4.dp +private val LeadingSize = IconSize + PairRing * 2 + /** - * Centered amount block of the single-asset card: token avatar (with network badge) over the big signed amount and the - * secondary fiat line. + * Centered amount block of the single-asset card: the token avatar (single, or a yield-supply asset+Aave pair), an + * optional label ("Supplied"/"Returned"), the big amount and the secondary fiat line. * * The failed state ([TxHistoryDetailsUM.AmountBlockUM.isFailed]) strikes the amount through and dims it (primary -> - * secondary) — matching the status-driven recolor of the shared header. The `+`/`−` sign is already dropped upstream - * by the converter for failed transactions (a failed tx moved nothing), so the [amount] text arrives unsigned here. + * secondary) — matching the status-driven recolor of the shared header. The `+`/`−` sign is resolved upstream by the + * converter (dropped for failed and yield-supply transactions), so the [amount] text arrives ready to render here. */ @Composable internal fun TxHistoryDetailsAmountBlock(amountBlock: TxHistoryDetailsUM.AmountBlockUM, modifier: Modifier = Modifier) { @@ -41,11 +53,17 @@ internal fun TxHistoryDetailsAmountBlock(amountBlock: TxHistoryDetailsUM.AmountB .padding(vertical = 48.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - TangemCurrencyIcon( - state = amountBlock.currencyIcon, - modifier = Modifier.size(72.dp), - ) + AmountIcon(amountBlock.icon) SpacerH(24.dp) + amountBlock.label?.let { label -> + Text( + text = label.resolveReference(), + color = TangemTheme.colors3.text.secondary, + style = TangemTheme.typography3.subheading.medium, + textAlign = TextAlign.Center, + ) + SpacerH(4.dp) + } Text( text = amountBlock.amount.resolveReference(), color = if (amountBlock.isFailed) { @@ -73,6 +91,60 @@ internal fun TxHistoryDetailsAmountBlock(amountBlock: TxHistoryDetailsUM.AmountB } } +@Composable +private fun AmountIcon(icon: TxHistoryDetailsUM.AmountIconUM, modifier: Modifier = Modifier) { + when (icon) { + is TxHistoryDetailsUM.AmountIconUM.Single -> AmountIconItem( + item = TxHistoryDetailsUM.AmountIconUM.Item.Currency(icon.currencyIcon), + modifier = modifier, + ) + is TxHistoryDetailsUM.AmountIconUM.OverlappingPair -> Box( + modifier = modifier + .height(LeadingSize) + .width(PairRing + IconSize * 2 - PairOverlap), + ) { + // Trailing behind, right; the pair drops the network badge so the two token arts read as equal-size icons. + AmountIconItem( + item = icon.trailing, + modifier = Modifier.align(Alignment.CenterEnd), + shouldDisplayNetwork = false, + ) + // Leading on top, left, wrapped in a background-colored ring that separates it from the trailing icon. + Box( + modifier = Modifier + .align(Alignment.CenterStart) + .size(LeadingSize) + .background(TangemTheme.colors3.bg.secondary, CircleShape) + .padding(PairRing), + ) { + AmountIconItem(item = icon.leading, shouldDisplayNetwork = false) + } + } + } +} + +@Composable +private fun AmountIconItem( + item: TxHistoryDetailsUM.AmountIconUM.Item, + modifier: Modifier = Modifier, + shouldDisplayNetwork: Boolean = true, +) { + when (item) { + is TxHistoryDetailsUM.AmountIconUM.Item.Currency -> TangemCurrencyIcon( + state = item.state, + modifier = modifier.size(IconSize), + shouldDisplayNetwork = shouldDisplayNetwork, + ) + is TxHistoryDetailsUM.AmountIconUM.Item.Resource -> Image( + painter = painterResource(item.resId), + contentDescription = null, + modifier = modifier + .size(IconSize) + .clip(CircleShape), + ) + } +} + // region Preview @Preview(name = "Light", showBackground = true, widthDp = 360) @@ -87,21 +159,42 @@ private fun TxHistoryDetailsAmountBlockPreview() { TxHistoryDetailsAmountBlock(amountBlock = previewAmountBlock(isFailed = true)) // No fiat — the fiat line is omitted entirely. TxHistoryDetailsAmountBlock(amountBlock = previewAmountBlock(isFailed = false, fiatAmount = null)) + // Yield supply — "Supplied" (Aave leads) and "Returned" (asset leads), unsigned amount. + TxHistoryDetailsAmountBlock( + amountBlock = previewAmountBlock( + isFailed = false, + fiatAmount = null, + icon = TxHistoryDetailsUM.AmountIconUM.OverlappingPair( + leading = TxHistoryDetailsUM.AmountIconUM.Item.Resource(R.drawable.img_aave_22), + trailing = TxHistoryDetailsUM.AmountIconUM.Item.Currency(previewCurrencyIcon()), + ), + label = stringReference("Supplied"), + amount = stringReference("1,294.23 USDT"), + ), + ) } } } -private fun previewAmountBlock(isFailed: Boolean, fiatAmount: TextReference? = stringReference("$350.31")) = - TxHistoryDetailsUM.AmountBlockUM( - currencyIcon = CurrencyIconState.CoinIcon( - url = null, - fallbackResId = R.drawable.img_eth_22, - isGrayscale = false, - shouldShowCustomBadge = false, - ), - amount = stringReference("+ 350.31 USDT"), - fiatAmount = fiatAmount, - isFailed = isFailed, - ) +private fun previewCurrencyIcon() = CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_eth_22, + isGrayscale = false, + shouldShowCustomBadge = false, +) + +private fun previewAmountBlock( + isFailed: Boolean, + fiatAmount: TextReference? = stringReference("$350.31"), + icon: TxHistoryDetailsUM.AmountIconUM = TxHistoryDetailsUM.AmountIconUM.Single(previewCurrencyIcon()), + label: TextReference? = null, + amount: TextReference = stringReference("+ 350.31 USDT"), +) = TxHistoryDetailsUM.AmountBlockUM( + icon = icon, + amount = amount, + label = label, + fiatAmount = fiatAmount, + isFailed = isFailed, +) // endregion \ No newline at end of file diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/ui/TxHistoryDetailsModalBottomSheetContent.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/ui/TxHistoryDetailsModalBottomSheetContent.kt index 9261a5ec0f..355be8711f 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/ui/TxHistoryDetailsModalBottomSheetContent.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/ui/TxHistoryDetailsModalBottomSheetContent.kt @@ -78,11 +78,13 @@ private fun previewSingleAsset() = TxHistoryDetailsUM.SingleAsset( menu = previewMenu(), ), amountBlock = TxHistoryDetailsUM.AmountBlockUM( - currencyIcon = CurrencyIconState.CoinIcon( - url = null, - fallbackResId = R.drawable.img_eth_22, - isGrayscale = false, - shouldShowCustomBadge = false, + icon = TxHistoryDetailsUM.AmountIconUM.Single( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_eth_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), ), amount = stringReference("- 350.31 USDT"), fiatAmount = stringReference("$350.31"), diff --git a/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/OnChainTxToDetailsUMConverterTest.kt b/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/OnChainTxToDetailsUMConverterTest.kt index ba0412e862..dca56d6ec5 100644 --- a/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/OnChainTxToDetailsUMConverterTest.kt +++ b/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/OnChainTxToDetailsUMConverterTest.kt @@ -17,6 +17,7 @@ import kotlinx.collections.immutable.persistentListOf import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource import java.math.BigDecimal @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -207,6 +208,55 @@ internal class OnChainTxToDetailsUMConverterTest : TxDetailsConverterTestBase() assertThat(amount).doesNotContain("-") } + @Test + fun `GIVEN non-yield Transfer WHEN convert THEN single icon and no label`() { + // Arrange + val tx = txInfo(type = TransactionType.Transfer) + + // Act + val amountBlock = converter.convert(tx).amountBlock + + // Assert + assertThat(amountBlock.icon).isInstanceOf(TxHistoryDetailsUM.AmountIconUM.Single::class.java) + assertThat(amountBlock.label).isNull() + } + + @Test + fun `GIVEN yield-supply Enter WHEN convert THEN Supplied label, Aave-leading pair and unsigned amount`() { + // Arrange — enter (funds outgoing to Aave): the amount must still be unsigned. + val tx = txInfo(type = TransactionType.YieldSupply.Enter(address = USER_ADDRESS), isOutgoing = true) + + // Act + val amountBlock = converter.convert(tx).amountBlock + + // Assert + assertThat(amountBlock.label).isEqualTo(resourceReference(R.string.yield_module_transaction_supplied)) + val pair = amountBlock.icon as TxHistoryDetailsUM.AmountIconUM.OverlappingPair + assertThat(pair.leading).isEqualTo(TxHistoryDetailsUM.AmountIconUM.Item.Resource(R.drawable.img_aave_22)) + assertThat(pair.trailing).isInstanceOf(TxHistoryDetailsUM.AmountIconUM.Item.Currency::class.java) + val amount = amountBlock.amount.resolveString() + assertThat(amount).doesNotContain("+") + assertThat(amount).doesNotContain("-") + } + + @Test + fun `GIVEN yield-supply Exit WHEN convert THEN Returned label, asset-leading pair and unsigned amount`() { + // Arrange + val tx = txInfo(type = TransactionType.YieldSupply.Exit(address = USER_ADDRESS), isOutgoing = false) + + // Act + val amountBlock = converter.convert(tx).amountBlock + + // Assert + assertThat(amountBlock.label).isEqualTo(resourceReference(R.string.yield_module_transaction_returned)) + val pair = amountBlock.icon as TxHistoryDetailsUM.AmountIconUM.OverlappingPair + assertThat(pair.leading).isInstanceOf(TxHistoryDetailsUM.AmountIconUM.Item.Currency::class.java) + assertThat(pair.trailing).isEqualTo(TxHistoryDetailsUM.AmountIconUM.Item.Resource(R.drawable.img_aave_22)) + val amount = amountBlock.amount.resolveString() + assertThat(amount).doesNotContain("+") + assertThat(amount).doesNotContain("-") + } + // endregion // region Counterparty @@ -223,6 +273,29 @@ internal class OnChainTxToDetailsUMConverterTest : TxDetailsConverterTestBase() assertThat(counterparty).isNull() } + @ParameterizedTest + @MethodSource("provideContractInteractionTypes") + fun `GIVEN contract-interaction tx with User interaction address WHEN convert THEN no counterparty card`( + type: TransactionType, + ) { + // Arrange — yield-supply / staking / approve talk to a protocol/validator, not a copyable recipient. + val tx = txInfo(type = type, interactionAddressType = TxInfo.InteractionAddressType.User(USER_ADDRESS)) + + // Act + val counterparty = converter.convert(tx).counterparty + + // Assert + assertThat(counterparty).isNull() + } + + private fun provideContractInteractionTypes() = listOf( + TransactionType.YieldSupply.Enter(address = USER_ADDRESS), + TransactionType.YieldSupply.Exit(address = USER_ADDRESS), + TransactionType.Staking.Stake, + TransactionType.Staking.Vote(validatorAddress = VALIDATOR_ADDRESS), + TransactionType.Approve, + ) + @Test fun `GIVEN incoming Transfer with User address WHEN convert THEN address-avatar counterparty with From label`() { // Arrange @@ -435,8 +508,8 @@ internal class OnChainTxToDetailsUMConverterTest : TxDetailsConverterTestBase() // region Protocol row (yield-supply) @Test - fun `GIVEN yield-supply tx WHEN convert THEN protocol row shows the hard-wired Aave protocol`() { - // Arrange — yield-supply is a single hard-wired integration (Aave), so the value is constant, not resolved. + fun `GIVEN yield-supply tx WHEN convert THEN protocol row shows the hard-wired Aave protocol with its link`() { + // Arrange — yield-supply is a single hard-wired integration (Aave), so the value and link are constant. val tx = txInfo(type = TransactionType.YieldSupply.Enter(address = USER_ADDRESS)) // Act @@ -445,8 +518,9 @@ internal class OnChainTxToDetailsUMConverterTest : TxDetailsConverterTestBase() // Assert assertThat(row.label).isEqualTo(resourceReference(R.string.staking_validator)) assertThat(row.value).isEqualTo(resourceReference(R.string.yield_module_provider)) - assertThat(row.trailingIconRes).isNull() - assertThat(row.onClick).isNull() + assertThat(row.trailingIconRes).isEqualTo(R.drawable.ic_arrow_top_right_24) + row.onClick?.invoke() + assertThat(openedUrls).containsExactly("https://aave.com/") } @Test