diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index e315735ecf..0c454a990d 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -2078,6 +2078,7 @@
Tap the twin card with number %s and do not remove until the end of the operation
Top up
Topped up
+ You paid
Please try again later. If the issue persists, please contact support.
Something went wrong!
We\'ve encountered an error. Error code: %s. Please contact our support.
diff --git a/features/txhistory/impl/build.gradle.kts b/features/txhistory/impl/build.gradle.kts
index 8bd1f5e086..7d678cb707 100644
--- a/features/txhistory/impl/build.gradle.kts
+++ b/features/txhistory/impl/build.gradle.kts
@@ -69,6 +69,7 @@ dependencies {
/* Tests */
testImplementation(projects.common.test)
testImplementation(projects.test.core)
+ testImplementation(projects.test.mock)
testImplementation(projects.domain.onramp.models)
testImplementation(deps.test.junit5)
testImplementation(deps.test.mockk)
diff --git a/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/ExpressTxToTransactionItemUMConverterTest.kt b/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/ExpressTxToTransactionItemUMConverterTest.kt
index 8719768595..f8ff49c6ff 100644
--- a/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/ExpressTxToTransactionItemUMConverterTest.kt
+++ b/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/ExpressTxToTransactionItemUMConverterTest.kt
@@ -220,6 +220,8 @@ internal class ExpressTxToTransactionItemUMConverterTest {
provider = null,
payinHash = null,
payoutHash = null,
+ fromAddress = null,
+ payoutAddress = null,
fromAsset = ExpressTransactionAsset(
id = ExpressAssetId(networkId = "eth", contractAddress = "0"),
amount = fromAmount,
@@ -245,6 +247,7 @@ internal class ExpressTxToTransactionItemUMConverterTest {
createdAtMillis = 100,
provider = null,
payoutHash = null,
+ payoutAddress = null,
fromFiat = Amount(
currencySymbol = "SEK",
value = BigDecimal("100"),
diff --git a/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryInfoToTransactionItemUMConverterTest.kt b/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryInfoToTransactionItemUMConverterTest.kt
index f1b8c4915e..ccf65e9aa9 100644
--- a/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryInfoToTransactionItemUMConverterTest.kt
+++ b/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryInfoToTransactionItemUMConverterTest.kt
@@ -108,6 +108,8 @@ internal class TxHistoryInfoToTransactionItemUMConverterTest {
provider = null,
payinHash = null,
payoutHash = null,
+ fromAddress = null,
+ payoutAddress = null,
fromAsset = ExpressTransactionAsset(
id = ExpressAssetId(networkId = "ethereum", contractAddress = "0"),
amount = BigDecimal("1.5"),
diff --git a/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryInfoToTxHistoryDetailsUMConverterTest.kt b/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryInfoToTxHistoryDetailsUMConverterTest.kt
index e92ab06cf2..5be02de5b6 100644
--- a/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryInfoToTxHistoryDetailsUMConverterTest.kt
+++ b/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryInfoToTxHistoryDetailsUMConverterTest.kt
@@ -1,11 +1,14 @@
package com.tangem.features.txhistory.converter
import android.text.format.DateFormat
+import androidx.compose.ui.graphics.Color
import com.google.common.truth.Truth.assertThat
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
import com.tangem.core.ui.components.transactions.state.TransactionItemUM
+import com.tangem.core.ui.ds.image.DeviceIconUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.express.models.ExchangeTransaction
import com.tangem.domain.express.models.ExpressAsset.ID as ExpressAssetId
import com.tangem.domain.express.models.ExpressExchangeStatus
@@ -14,17 +17,23 @@ import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressProviderType
import com.tangem.domain.express.models.ExpressTransactionAsset
import com.tangem.domain.express.models.OnrampTransaction
+import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
+import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.SdkAmount
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.models.network.TxInfo.TransactionType
+import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.AmountType
import com.tangem.domain.txhistory.model.ExpressTx
import com.tangem.domain.txhistory.model.OnChainTx
import com.tangem.features.txhistory.entity.TxHistoryDetailsUM
import com.tangem.features.txhistory.impl.R
+import com.tangem.features.txhistory.model.TxHistoryLookupContext
+import com.tangem.features.txhistory.model.WalletInfo
import com.tangem.test.core.ProvideTestModels
+import com.tangem.test.mock.MockAccounts
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
@@ -43,6 +52,7 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverterTest {
// The express payout leg: a real Bitcoin coin so the resolved symbol (BTC) matches the "bitcoin" network id.
private val bitcoin = mockCurrencyFactory.bitcoin
+ private val ownAccount: Account.CryptoPortfolio = MockAccounts.createAccount(derivationIndex = 1, name = "Family")
private val copiedAddresses = mutableListOf()
private val openedUrls = mutableListOf()
private val converter = TxHistoryInfoToTxHistoryDetailsUMConverter(
@@ -145,7 +155,7 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverterTest {
currency = currency,
onCopyAddress = copiedAddresses::add,
onGoToProvider = openedUrls::add,
- ownAddresses = setOf(USER_ADDRESS),
+ lookup = lookupOf(currency.network.id.rawId to mapOf(USER_ADDRESS to ownAccount)),
)
val tx = onChain(
type = TransactionType.Transfer,
@@ -167,7 +177,7 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverterTest {
currency = currency,
onCopyAddress = copiedAddresses::add,
onGoToProvider = openedUrls::add,
- ownAddresses = setOf(USER_ADDRESS),
+ lookup = lookupOf(currency.network.id.rawId to mapOf(USER_ADDRESS to ownAccount)),
)
val tx = onChain(
type = TransactionType.Transfer,
@@ -736,6 +746,96 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverterTest {
// endregion
+ // region Express leg owner
+
+ @Test
+ fun `GIVEN swap between own accounts WHEN convert THEN legs labelled From-To with account owners`() {
+ // Arrange — from leg on ethereum, payout leg on bitcoin, both addresses owned, accounts mode on.
+ val swap = expressSwap(
+ status = ExpressExchangeStatus.Finished,
+ fromAddress = FROM_ADDRESS,
+ payoutAddress = PAYOUT_ADDRESS,
+ fromCurrency = currency,
+ )
+ val lookup = lookupOf(
+ currency.network.id.rawId to mapOf(FROM_ADDRESS to ownAccount),
+ bitcoin.network.id.rawId to mapOf(PAYOUT_ADDRESS to ownAccount),
+ )
+
+ // Act
+ val result = ownConverter(lookup).convert(swap) as TxHistoryDetailsUM.TwoAssets
+
+ // Assert
+ assertThat(result.from?.label).isEqualTo(resourceReference(R.string.common_from))
+ assertThat(result.to?.label).isEqualTo(resourceReference(R.string.common_to))
+ assertThat(result.from?.owner).isInstanceOf(TxHistoryDetailsUM.AssetOwnerUM.Account::class.java)
+ assertThat(result.to?.owner).isInstanceOf(TxHistoryDetailsUM.AssetOwnerUM.Account::class.java)
+ }
+
+ @Test
+ fun `GIVEN swap to own address with accounts mode off WHEN convert THEN owner is wallet`() {
+ // Arrange
+ val swap = expressSwap(status = ExpressExchangeStatus.Finished, payoutAddress = PAYOUT_ADDRESS)
+ val lookup = lookupOf(
+ bitcoin.network.id.rawId to mapOf(PAYOUT_ADDRESS to ownAccount),
+ isAccountsModeEnabled = false,
+ )
+
+ // Act
+ val result = ownConverter(lookup).convert(swap) as TxHistoryDetailsUM.TwoAssets
+
+ // Assert
+ val owner = result.to?.owner
+ assertThat(owner).isInstanceOf(TxHistoryDetailsUM.AssetOwnerUM.Wallet::class.java)
+ assertThat((owner as TxHistoryDetailsUM.AssetOwnerUM.Wallet).name).isEqualTo(stringReference("My Wallet"))
+ }
+
+ @Test
+ fun `GIVEN send-and-swap to external address WHEN convert THEN to leg owner is external address`() {
+ // Arrange — payout address is none of the user's, so it stays an external address.
+ val swap = expressSwap(status = ExpressExchangeStatus.Finished, payoutAddress = EXTERNAL_ADDRESS)
+
+ // Act
+ val result = ownConverter(lookupOf()).convert(swap) as TxHistoryDetailsUM.TwoAssets
+
+ // Assert
+ val owner = result.to?.owner
+ assertThat(owner).isInstanceOf(TxHistoryDetailsUM.AssetOwnerUM.Address::class.java)
+ assertThat((owner as TxHistoryDetailsUM.AssetOwnerUM.Address).rawAddress).isEqualTo(EXTERNAL_ADDRESS)
+ assertThat(result.to?.label).isEqualTo(resourceReference(R.string.common_to))
+ }
+
+ @Test
+ fun `GIVEN swap without addresses WHEN convert THEN no owner and default labels`() {
+ // Act — no fromAddress / payoutAddress plumbed (e.g. very old app version).
+ val result = converter.convert(expressSwap(status = ExpressExchangeStatus.Finished))
+ as TxHistoryDetailsUM.TwoAssets
+
+ // Assert
+ assertThat(result.from?.owner).isNull()
+ assertThat(result.to?.owner).isNull()
+ assertThat(result.from?.label).isEqualTo(resourceReference(R.string.swapping_from_title_v2))
+ assertThat(result.to?.label).isEqualTo(resourceReference(R.string.swapping_to_title))
+ }
+
+ @Test
+ fun `GIVEN onramp to own account WHEN convert THEN from is You paid and to has account owner`() {
+ // Arrange
+ val onramp = expressOnramp(status = ExpressOnrampStatus.Finished, payoutAddress = PAYOUT_ADDRESS)
+ val lookup = lookupOf(bitcoin.network.id.rawId to mapOf(PAYOUT_ADDRESS to ownAccount))
+
+ // Act
+ val result = ownConverter(lookup).convert(onramp) as TxHistoryDetailsUM.TwoAssets
+
+ // Assert
+ assertThat(result.from?.owner).isNull()
+ assertThat(result.from?.label).isEqualTo(resourceReference(R.string.tx_history_you_paid))
+ assertThat(result.to?.owner).isInstanceOf(TxHistoryDetailsUM.AssetOwnerUM.Account::class.java)
+ assertThat(result.to?.label).isEqualTo(resourceReference(R.string.common_to))
+ }
+
+ // endregion
+
private fun onChain(
type: TransactionType,
isOutgoing: Boolean = false,
@@ -774,6 +874,9 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverterTest {
txInfo: OnChainTx? = null,
provider: ExpressProvider? = null,
externalTxUrl: String? = null,
+ fromAddress: String? = null,
+ payoutAddress: String? = null,
+ fromCurrency: CryptoCurrency? = null,
): ExpressTx.Swap = ExpressTx.Swap(
tx = ExchangeTransaction(
txId = "swap-1",
@@ -782,7 +885,14 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverterTest {
provider = provider,
payinHash = null,
payoutHash = null,
- fromAsset = expressAsset(networkId = "ethereum", amount = BigDecimal("1.5"), decimals = 18),
+ fromAddress = fromAddress,
+ payoutAddress = payoutAddress,
+ fromAsset = expressAsset(
+ networkId = "ethereum",
+ amount = BigDecimal("1.5"),
+ decimals = 18,
+ cryptoCurrency = fromCurrency,
+ ),
toAsset = expressAsset(
networkId = "bitcoin",
amount = BigDecimal("0.001"),
@@ -799,6 +909,7 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverterTest {
status: ExpressOnrampStatus,
txInfo: OnChainTx? = null,
externalTxUrl: String? = null,
+ payoutAddress: String? = null,
): ExpressTx.Onramp = ExpressTx.Onramp(
tx = OnrampTransaction(
txId = "onramp-1",
@@ -806,6 +917,7 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverterTest {
createdAtMillis = TIMESTAMP,
provider = null,
payoutHash = null,
+ payoutAddress = payoutAddress,
externalTxUrl = externalTxUrl,
fromFiat = Amount(
currencySymbol = "SEK",
@@ -836,6 +948,29 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverterTest {
cryptoCurrency = cryptoCurrency,
)
+ /** Builds a details lookup with the given per-network own-address maps. */
+ private fun lookupOf(
+ vararg networks: Pair>,
+ isAccountsModeEnabled: Boolean = true,
+ walletInfoById: Map = mapOf(
+ MockAccounts.userWalletId to WalletInfo(
+ name = "My Wallet",
+ deviceIconUM = DeviceIconUM.Card(mainColor = Color(0xFF1E1E1E), secondColor = null),
+ ),
+ ),
+ ): TxHistoryLookupContext = TxHistoryLookupContext(
+ ownAccountByNetwork = networks.toMap(),
+ isAccountsModeEnabled = isAccountsModeEnabled,
+ walletInfoById = walletInfoById,
+ )
+
+ private fun ownConverter(lookup: TxHistoryLookupContext) = TxHistoryInfoToTxHistoryDetailsUMConverter(
+ currency = currency,
+ onCopyAddress = copiedAddresses::add,
+ onGoToProvider = openedUrls::add,
+ lookup = lookup,
+ )
+
private fun TextReference.resolveString(): String = (this as TextReference.Str).value
private companion object {
@@ -844,5 +979,8 @@ internal class TxHistoryInfoToTxHistoryDetailsUMConverterTest {
const val USER_ADDRESS = "0x1234567890abcdef1234"
const val VALIDATOR_ADDRESS = "0xvalidator"
const val EXTERNAL_URL = "https://provider.example/tx/swap-1"
+ const val FROM_ADDRESS = "0xfromOwnAddress1234"
+ const val PAYOUT_ADDRESS = "bc1qPayoutOwnAddress"
+ const val EXTERNAL_ADDRESS = "bc1qExternalNonUserAddress"
}
}
\ No newline at end of file
diff --git a/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverterTest.kt b/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverterTest.kt
index ed577e236b..234928cdf1 100644
--- a/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverterTest.kt
+++ b/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverterTest.kt
@@ -268,7 +268,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
currency = coin,
txHistoryUiActions = txHistoryUiActions,
lookupContext = TxHistoryLookupContext(
- ownAccountByAddress = mapOf(USER_ADDRESS to ownAccount),
+ ownAccountByNetwork = mapOf(coin.network.id.rawId to mapOf(USER_ADDRESS to ownAccount)),
isAccountsModeEnabled = true,
walletInfoById = emptyMap(),
),
@@ -296,7 +296,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
currency = coin,
txHistoryUiActions = txHistoryUiActions,
lookupContext = TxHistoryLookupContext(
- ownAccountByAddress = mapOf(USER_ADDRESS to ownAccount),
+ ownAccountByNetwork = mapOf(coin.network.id.rawId to mapOf(USER_ADDRESS to ownAccount)),
isAccountsModeEnabled = false,
walletInfoById = mapOf(userWalletId to walletInfo),
),
@@ -323,7 +323,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
currency = coin,
txHistoryUiActions = txHistoryUiActions,
lookupContext = TxHistoryLookupContext(
- ownAccountByAddress = mapOf(USER_ADDRESS to ownAccount),
+ ownAccountByNetwork = mapOf(coin.network.id.rawId to mapOf(USER_ADDRESS to ownAccount)),
isAccountsModeEnabled = false,
walletInfoById = emptyMap(),
),
diff --git a/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/utils/TxHistoryInfoMergerTest.kt b/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/utils/TxHistoryInfoMergerTest.kt
index f4a022d2c1..fd87fa1aaa 100644
--- a/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/utils/TxHistoryInfoMergerTest.kt
+++ b/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/utils/TxHistoryInfoMergerTest.kt
@@ -110,6 +110,8 @@ internal class TxHistoryInfoMergerTest {
provider = null,
payinHash = matchHash.takeIf { isOutgoing },
payoutHash = matchHash.takeUnless { isOutgoing },
+ fromAddress = null,
+ payoutAddress = null,
fromAsset = ExpressTransactionAsset(
id = ExpressAssetId(networkId = "eth", contractAddress = "0"),
amount = BigDecimal("1.5"),