Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-15 16:56:05 +04:00
parent 9f070d6023
commit b7c264b448
12 changed files with 408 additions and 13 deletions

View file

@ -0,0 +1,52 @@
package com.tangem.features.walletconnect.transaction.converter
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.walletconnect.model.WcBitcoinMethod
import com.tangem.features.walletconnect.impl.R
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestBlockUM
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoItemUM
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
import javax.inject.Inject
/**
* Builds a transaction request details block (recipient and amount) for a Bitcoin `sendTransfer` WalletConnect request.
*
*/
internal class WcBtcSendTransferRequestInfoConverter @Inject constructor() :
Converter<WcBtcSendTransferRequestInfoConverter.Input, WcTransactionRequestBlockUM> {
override fun convert(value: Input): WcTransactionRequestBlockUM {
val method = value.method
return WcTransactionRequestBlockUM(
info = buildList {
add(WcTransactionRequestInfoItemUM(resourceReference(R.string.common_from), method.account))
add(WcTransactionRequestInfoItemUM(resourceReference(R.string.common_to), method.recipientAddress))
add(
WcTransactionRequestInfoItemUM(
title = resourceReference(R.string.common_amount),
description = formatAmount(method.amount, value.decimals, value.symbol),
),
)
val changeAddress = method.changeAddress
if (!changeAddress.isNullOrEmpty()) {
add(WcTransactionRequestInfoItemUM(resourceReference(R.string.wc_change_address), changeAddress))
}
}.toImmutableList(),
)
}
private fun formatAmount(amount: String, decimals: Int, symbol: String): String {
val value = amount.toBigDecimalOrNull()
?.movePointLeft(decimals)
?.stripTrailingZeros()
?: return amount
return "${value.toPlainString()} $symbol"
}
data class Input(
val method: WcBitcoinMethod.SendTransfer,
val decimals: Int,
val symbol: String,
)
}

View file

@ -6,6 +6,7 @@ import com.tangem.domain.models.wallet.isHotWallet
import com.tangem.domain.walletconnect.model.WcBitcoinMethod
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.model.WcMethod
import com.tangem.domain.walletconnect.model.WcPsbtOutput
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck
import com.tangem.domain.walletconnect.usecase.method.WcMethodContext
@ -15,11 +16,13 @@ import com.tangem.features.send.api.entity.FeeSelectorUM
import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionFeeState
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestBlockUM
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM
import com.tangem.features.walletconnect.transaction.entity.send.WcSendTransactionItemUM
import com.tangem.features.walletconnect.transaction.entity.send.WcSendTransactionUM
import com.tangem.features.walletconnect.utils.WcNotificationsFactory
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import javax.inject.Inject
@ -27,6 +30,8 @@ internal class WcSendTransactionUMConverter @Inject constructor(
private val appInfoContentUMConverter: WcTransactionAppInfoContentUMConverter,
private val networkInfoUMConverter: WcNetworkInfoUMConverter,
private val requestBlockUMConverter: WcTransactionRequestBlockUMConverter,
private val btcSendTransferRequestInfoConverter: WcBtcSendTransferRequestInfoConverter,
private val signPsbtRequestInfoConverter: WcSignPsbtRequestInfoConverter,
private val notificationsFactory: WcNotificationsFactory,
) : Converter<WcSendTransactionUMConverter.Input, WcSendTransactionUM?> {
@ -76,13 +81,7 @@ internal class WcSendTransactionUMConverter @Inject constructor(
is WcTransactionFeeState.Success -> value.feeSelectorUM ?: FeeSelectorUM.Loading
},
transactionRequestInfo = WcTransactionRequestInfoUM(
blocks = buildList {
addAll(
requestBlockUMConverter.convert(
WcTransactionRequestBlockUMConverter.Input(value.context.rawSdkRequest),
),
)
}.toImmutableList(),
blocks = buildRequestInfoBlocks(value),
onCopy = value.actions.onCopy,
),
)
@ -97,6 +96,37 @@ internal class WcSendTransactionUMConverter @Inject constructor(
}
}
private fun buildRequestInfoBlocks(value: Input): ImmutableList<WcTransactionRequestBlockUM> = buildList {
addAll(
requestBlockUMConverter.convert(
WcTransactionRequestBlockUMConverter.Input(value.context.rawSdkRequest),
),
)
val method = value.context.method
if (method is WcBitcoinMethod.SendTransfer) {
add(
btcSendTransferRequestInfoConverter.convert(
WcBtcSendTransferRequestInfoConverter.Input(
method = method,
decimals = value.cryptoCurrencyStatus.currency.decimals,
symbol = value.cryptoCurrencyStatus.currency.symbol,
),
),
)
}
if (method is WcBitcoinMethod.SignPsbt && value.psbtOutputs != null) {
addAll(
signPsbtRequestInfoConverter.convert(
WcSignPsbtRequestInfoConverter.Input(
outputs = value.psbtOutputs,
decimals = value.cryptoCurrencyStatus.currency.decimals,
symbol = value.cryptoCurrencyStatus.currency.symbol,
),
),
)
}
}.toImmutableList()
data class Input(
val context: WcMethodContext,
val portfolioName: AccountTitleUM?,
@ -106,6 +136,7 @@ internal class WcSendTransactionUMConverter @Inject constructor(
val feeSelectorUM: FeeSelectorUM?,
val cryptoCurrencyStatus: CryptoCurrencyStatus,
val securityCheck: BlockAidTransactionCheck.Result?,
val psbtOutputs: List<WcPsbtOutput>?,
val onFeeReload: () -> Unit,
)
}

View file

@ -0,0 +1,66 @@
package com.tangem.features.walletconnect.transaction.converter
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.walletconnect.model.WcPsbtOutput
import com.tangem.features.walletconnect.impl.R
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestBlockUM
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoItemUM
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
import javax.inject.Inject
/**
* Builds transaction request details blocks (recipient + amount per output) for a Bitcoin `signPsbt` request.
*
*/
internal class WcSignPsbtRequestInfoConverter @Inject constructor() :
Converter<WcSignPsbtRequestInfoConverter.Input, List<WcTransactionRequestBlockUM>> {
override fun convert(value: Input): List<WcTransactionRequestBlockUM> {
val outputs = value.outputs
if (outputs.isEmpty()) return emptyList()
val shouldShowIndex = outputs.size > 1
return outputs.mapIndexed { index, output ->
val recipientTitle = when {
output.address == null -> resourceReference(R.string.common_no_address)
shouldShowIndex -> combinedReference(
resourceReference(R.string.common_to),
stringReference(" ${index + 1}"),
)
else -> resourceReference(R.string.common_to)
}
WcTransactionRequestBlockUM(
info = buildList {
add(
WcTransactionRequestInfoItemUM(
title = recipientTitle,
description = output.address.orEmpty(),
),
)
add(
WcTransactionRequestInfoItemUM(
title = resourceReference(R.string.common_amount),
description = formatAmount(output.amountSatoshi, value.decimals, value.symbol),
),
)
}.toImmutableList(),
)
}
}
private fun formatAmount(amountSatoshi: Long, decimals: Int, symbol: String): String {
val value = BigDecimal.valueOf(amountSatoshi)
.movePointLeft(decimals)
.stripTrailingZeros()
return "${value.toPlainString()} $symbol"
}
data class Input(
val outputs: List<WcPsbtOutput>,
val decimals: Int,
val symbol: String,
)
}

View file

@ -38,6 +38,7 @@ import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.WcAnalyticEvents.SignatureRequestReceived.EmulationStatus
import com.tangem.domain.walletconnect.WcAnalyticEvents.SolanaLargeTransaction
import com.tangem.domain.walletconnect.WcRequestUseCaseFactory
import com.tangem.domain.walletconnect.model.WcPsbtOutput
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.WcRequestError.Companion.message
import com.tangem.domain.walletconnect.usecase.method.*
@ -100,6 +101,7 @@ internal class WcSendTransactionModel @Inject constructor(
private var useCase: WcSignUseCase<*> by Delegates.notNull()
private var signState: WcSignState<*> by Delegates.notNull()
private var wcApproval: WcApproval? = null
private var psbtOutputs: List<WcPsbtOutput>? = null
private var sign: () -> Unit = {}
private val blockAidUiConverter = WcSendAndReceiveBlockAidUiConverter()
private val feeReloadState = MutableStateFlow(false)
@ -126,6 +128,9 @@ internal class WcSendTransactionModel @Inject constructor(
.onNone { unknownMethodRunnable() }
.getOrNull() ?: return@launch
this@WcSendTransactionModel.useCase = useCase
if (useCase is WcPsbtUseCase) {
psbtOutputs = useCase.parsePsbtOutputs()
}
(useCase as? WcMutableFee)
?.dAppFee()
?.let { dAppFee ->
@ -283,6 +288,7 @@ internal class WcSendTransactionModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
onFeeReload = ::triggerFeeReload,
securityCheck = securityCheck.getOrNull(),
psbtOutputs = psbtOutputs,
portfolioName = portfolioNameDelegate.createAccountTitleUM(useCase.session),
),
)

View file

@ -0,0 +1,106 @@
package com.tangem.features.walletconnect.transaction.converter
import com.google.common.truth.Truth
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.walletconnect.model.WcBitcoinMethod
import com.tangem.features.walletconnect.impl.R
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestBlockUM
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoItemUM
import kotlinx.collections.immutable.toImmutableList
import org.junit.jupiter.api.Test
class WcBtcSendTransferRequestInfoConverterTest {
private val converter = WcBtcSendTransferRequestInfoConverter()
@Test
fun `GIVEN sendTransfer with change address WHEN convert THEN block with from to amount and change`() {
val input = WcBtcSendTransferRequestInfoConverter.Input(
method = WcBitcoinMethod.SendTransfer(
account = "bc1qsenderaddress",
recipientAddress = "bc1qrecipientaddress",
amount = "5000000",
memo = null,
changeAddress = "bc1qchangeaddress",
),
decimals = 8,
symbol = "BTC",
)
val expected = WcTransactionRequestBlockUM(
info = listOf(
WcTransactionRequestInfoItemUM(resourceReference(R.string.common_from), "bc1qsenderaddress"),
WcTransactionRequestInfoItemUM(resourceReference(R.string.common_to), "bc1qrecipientaddress"),
WcTransactionRequestInfoItemUM(resourceReference(R.string.common_amount), "0.05 BTC"),
WcTransactionRequestInfoItemUM(resourceReference(R.string.wc_change_address), "bc1qchangeaddress"),
).toImmutableList(),
)
Truth.assertThat(converter.convert(input)).isEqualTo(expected)
}
@Test
fun `GIVEN sendTransfer without change address WHEN convert THEN block with from to amount only`() {
val input = WcBtcSendTransferRequestInfoConverter.Input(
method = WcBitcoinMethod.SendTransfer(
account = "bc1qsenderaddress",
recipientAddress = "bc1qrecipientaddress",
amount = "100000000",
memo = null,
changeAddress = null,
),
decimals = 8,
symbol = "BTC",
)
val expected = WcTransactionRequestBlockUM(
info = listOf(
WcTransactionRequestInfoItemUM(resourceReference(R.string.common_from), "bc1qsenderaddress"),
WcTransactionRequestInfoItemUM(resourceReference(R.string.common_to), "bc1qrecipientaddress"),
WcTransactionRequestInfoItemUM(resourceReference(R.string.common_amount), "1 BTC"),
).toImmutableList(),
)
Truth.assertThat(converter.convert(input)).isEqualTo(expected)
}
@Test
fun `GIVEN blank change address WHEN convert THEN change address item is omitted`() {
val input = WcBtcSendTransferRequestInfoConverter.Input(
method = WcBitcoinMethod.SendTransfer(
account = "bc1qsenderaddress",
recipientAddress = "bc1qrecipientaddress",
amount = "1",
memo = null,
changeAddress = "",
),
decimals = 8,
symbol = "BTC",
)
val result = converter.convert(input)
Truth.assertThat(result.info).hasSize(3)
Truth.assertThat(result.info.last().title).isEqualTo(resourceReference(R.string.common_amount))
}
@Test
fun `GIVEN non-numeric amount WHEN convert THEN raw amount is shown`() {
val input = WcBtcSendTransferRequestInfoConverter.Input(
method = WcBitcoinMethod.SendTransfer(
account = "bc1qsenderaddress",
recipientAddress = "bc1qrecipientaddress",
amount = "not-a-number",
memo = null,
changeAddress = null,
),
decimals = 8,
symbol = "BTC",
)
val amountItem = converter.convert(input).info[2]
Truth.assertThat(amountItem.title).isEqualTo(resourceReference(R.string.common_amount))
Truth.assertThat(amountItem.description).isEqualTo("not-a-number")
}
}

View file

@ -0,0 +1,82 @@
package com.tangem.features.walletconnect.transaction.converter
import com.google.common.truth.Truth
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.walletconnect.model.WcPsbtOutput
import com.tangem.features.walletconnect.impl.R
import org.junit.jupiter.api.Test
class WcSignPsbtRequestInfoConverterTest {
private val converter = WcSignPsbtRequestInfoConverter()
@Test
fun `GIVEN single output WHEN convert THEN one block with to and amount`() {
val input = WcSignPsbtRequestInfoConverter.Input(
outputs = listOf(WcPsbtOutput(address = "bc1qrecipient", amountSatoshi = 100_000L)),
decimals = 8,
symbol = "BTC",
)
val result = converter.convert(input)
Truth.assertThat(result).hasSize(1)
Truth.assertThat(result[0].info.map { it.title }).containsExactly(
resourceReference(R.string.common_to),
resourceReference(R.string.common_amount),
).inOrder()
Truth.assertThat(result[0].info[0].description).isEqualTo("bc1qrecipient")
Truth.assertThat(result[0].info[1].description).isEqualTo("0.001 BTC")
}
@Test
fun `GIVEN multiple outputs WHEN convert THEN indexed blocks per output`() {
val input = WcSignPsbtRequestInfoConverter.Input(
outputs = listOf(
WcPsbtOutput(address = "bc1qfirst", amountSatoshi = 250_000L),
WcPsbtOutput(address = "bc1qsecond", amountSatoshi = 1_0000_0000L),
),
decimals = 8,
symbol = "BTC",
)
val result = converter.convert(input)
Truth.assertThat(result).hasSize(2)
Truth.assertThat(result[0].info[0].title)
.isEqualTo(combinedReference(resourceReference(R.string.common_to), stringReference(" 1")))
Truth.assertThat(result[0].info[1].description).isEqualTo("0.0025 BTC")
Truth.assertThat(result[1].info[0].title)
.isEqualTo(combinedReference(resourceReference(R.string.common_to), stringReference(" 2")))
Truth.assertThat(result[1].info[0].description).isEqualTo("bc1qsecond")
Truth.assertThat(result[1].info[1].description).isEqualTo("1 BTC")
}
@Test
fun `GIVEN output with undecodable address WHEN convert THEN no address title and empty recipient`() {
val input = WcSignPsbtRequestInfoConverter.Input(
outputs = listOf(WcPsbtOutput(address = null, amountSatoshi = 50_000L)),
decimals = 8,
symbol = "BTC",
)
val result = converter.convert(input)
Truth.assertThat(result[0].info[0].title).isEqualTo(resourceReference(R.string.common_no_address))
Truth.assertThat(result[0].info[0].description).isEqualTo("")
Truth.assertThat(result[0].info[1].description).isEqualTo("0.0005 BTC")
}
@Test
fun `GIVEN no outputs WHEN convert THEN empty list`() {
val input = WcSignPsbtRequestInfoConverter.Input(
outputs = emptyList(),
decimals = 8,
symbol = "BTC",
)
Truth.assertThat(converter.convert(input)).isEmpty()
}
}