Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-29 17:21:54 +03:00
parent dd0a223583
commit 3503cadb5e
7 changed files with 153 additions and 6 deletions

View file

@ -2078,6 +2078,7 @@
<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="tx_history_you_paid">You paid</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>

View file

@ -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)

View file

@ -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"),

View file

@ -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"),

View file

@ -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<String>()
private val openedUrls = mutableListOf<String>()
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<Network.RawID, Map<String, Account.CryptoPortfolio>>,
isAccountsModeEnabled: Boolean = true,
walletInfoById: Map<UserWalletId, WalletInfo> = 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"
}
}

View file

@ -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(),
),

View file

@ -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"),