Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-20 12:47:25 +03:00
commit c50840894f
739 changed files with 27664 additions and 4701 deletions

View file

@ -21,6 +21,7 @@ import com.tangem.features.walletconnect.components.WcRoutingComponent
import com.tangem.features.walletconnect.connections.components.AlertsComponent
import com.tangem.features.walletconnect.connections.components.AlertsComponent.AlertType.*
import com.tangem.features.walletconnect.connections.components.WcPairComponent
import com.tangem.features.walletconnect.transaction.components.addresses.WcGetAddressesComponent
import com.tangem.features.walletconnect.transaction.components.chain.WcAddNetworkContainerComponent
import com.tangem.features.walletconnect.transaction.components.chain.WcSwitchNetworkComponent
import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams
@ -79,6 +80,10 @@ internal class DefaultWcRoutingComponent @AssistedInject constructor(
appComponentContext = childContext,
params = WcTransactionModelParams(config.rawRequest),
)
is WcInnerRoute.GetAddresses -> WcGetAddressesComponent(
appComponentContext = childContext,
params = WcTransactionModelParams(config.rawRequest),
)
is WcInnerRoute.Send -> WcSendTransactionContainerComponent(
appComponentContext = childContext,
params = WcTransactionModelParams(config.rawRequest),

View file

@ -25,6 +25,9 @@ internal sealed interface WcInnerRoute : Route {
@Serializable
data class SwitchNetwork(override val rawRequest: WcSdkSessionRequest) : Method
@Serializable
data class GetAddresses(override val rawRequest: WcSdkSessionRequest) : Method
@Serializable
data class Pair(val request: WcPairRequest) : WcInnerRoute

View file

@ -7,11 +7,13 @@ import com.tangem.core.decompose.model.Model
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.WcRequestService
import com.tangem.domain.walletconnect.model.WcBitcoinMethodName
import com.tangem.domain.walletconnect.model.WcEthMethodName
import com.tangem.domain.walletconnect.model.WcMethodName
import com.tangem.domain.walletconnect.model.WcSolanaMethodName
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
@ModelScoped
@ -32,6 +34,7 @@ internal class WcRoutingModel @Inject constructor(
}
fun onSlotEmpty() {
TangemLogger.d("WC Queue: onSlotEmpty() called")
isSlotEmpty.update { true }
}
@ -44,18 +47,35 @@ internal class WcRoutingModel @Inject constructor(
WcEthMethodName.SignTypeData,
WcEthMethodName.SignTypeDataV4,
WcSolanaMethodName.SignMessage,
-> WcInnerRoute.SignMessage(rawRequest)
WcBitcoinMethodName.SignMessage,
-> {
WcInnerRoute.SignMessage(rawRequest)
}
WcEthMethodName.AddEthereumChain,
-> WcInnerRoute.AddNetwork(rawRequest)
-> {
WcInnerRoute.AddNetwork(rawRequest)
}
WcEthMethodName.SwitchEthereumChain,
-> WcInnerRoute.SwitchNetwork(rawRequest)
-> {
WcInnerRoute.SwitchNetwork(rawRequest)
}
WcEthMethodName.SignTransaction,
WcEthMethodName.SendTransaction,
WcSolanaMethodName.SignTransaction,
WcSolanaMethodName.SendAllTransaction,
-> WcInnerRoute.Send(rawRequest)
WcBitcoinMethodName.SendTransfer,
WcBitcoinMethodName.SignPsbt,
-> {
WcInnerRoute.Send(rawRequest)
}
WcBitcoinMethodName.GetAccountAddresses,
-> {
WcInnerRoute.GetAddresses(rawRequest)
}
is WcMethodName.Unsupported,
-> WcInnerRoute.UnsupportedMethodAlert
-> {
WcInnerRoute.UnsupportedMethodAlert
}
}
}
@ -64,7 +84,9 @@ internal class WcRoutingModel @Inject constructor(
merge(requestFlow, pairFlow)
.onEach { configuration ->
TangemLogger.d("WC Queue: Received configuration $configuration, waiting for queue ready")
awaitQueueReady()
TangemLogger.d("WC Queue: Queue ready, pushing configuration")
isSlotEmpty.update { false }
innerRouter.push(configuration)
}
@ -76,7 +98,12 @@ internal class WcRoutingModel @Inject constructor(
permittedAppRoute,
cardSdkProvider.sdk.uiVisibility(),
) { isSlotEmpty, permittedAppRoute, isCardSdkVisible ->
isSlotEmpty && permittedAppRoute && !isCardSdkVisible
val isReady = isSlotEmpty && permittedAppRoute && !isCardSdkVisible
TangemLogger.d(
"WC Queue: isSlotEmpty=$isSlotEmpty, permittedAppRoute=$permittedAppRoute, " +
"isCardSdkVisible=$isCardSdkVisible, ready=$isReady",
)
isReady
}.first { it }
fun onAppRouteChange(appRoute: AppRoute) {

View file

@ -5,6 +5,7 @@ import com.tangem.core.decompose.model.Model
import com.tangem.features.walletconnect.connections.model.*
import com.tangem.features.walletconnect.connections.routing.WcRoutingModel
import com.tangem.features.walletconnect.transaction.model.WcAddNetworkModel
import com.tangem.features.walletconnect.transaction.model.WcGetAddressesModel
import com.tangem.features.walletconnect.transaction.model.WcSendTransactionModel
import com.tangem.features.walletconnect.transaction.model.WcSignTransactionModel
import com.tangem.features.walletconnect.transaction.model.WcSwitchNetworkModel
@ -53,6 +54,11 @@ internal interface WalletConnectModelModule {
@ClassKey(WcAddNetworkModel::class)
fun bindWcAddNetworkModel(model: WcAddNetworkModel): Model
@Binds
@IntoMap
@ClassKey(WcGetAddressesModel::class)
fun bindWcGetAddressesModel(model: WcGetAddressesModel): Model
@Binds
@IntoMap
@ClassKey(WcSwitchNetworkModel::class)

View file

@ -0,0 +1,25 @@
package com.tangem.features.walletconnect.transaction.components.addresses
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams
import com.tangem.features.walletconnect.transaction.model.WcGetAddressesModel
/**
* Component for Bitcoin getAccountAddresses WalletConnect method.
*/
internal class WcGetAddressesComponent(
appComponentContext: AppComponentContext,
params: WcTransactionModelParams,
) : AppComponentContext by appComponentContext, ComposableContentComponent {
@Suppress("UnusedPrivateProperty")
private val model: WcGetAddressesModel = getOrCreateModel(params = params)
@Composable
override fun Content(modifier: Modifier) {
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.core.ui.extensions.TextReference
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 com.tangem.utils.logging.TangemLogger
import kotlinx.collections.immutable.toImmutableList
import org.json.JSONArray
import org.json.JSONObject
@ -58,7 +59,16 @@ internal class TransactionParamsConverter @Inject constructor() : Converter<Stri
}
}
}
loop(JSONArray(value))
try {
when (value.trimStart().firstOrNull()) {
'[' -> loop(JSONArray(value))
'{' -> loop(JSONObject(value))
else -> loop(JSONArray(value)) // default to array for backward compatibility
}
} catch (e: Exception) {
TangemLogger.withTag("Wallet Connect").e("Failed to parse transaction params: ${e.message.orEmpty()}")
}
return result
}

View file

@ -2,6 +2,7 @@ package com.tangem.features.walletconnect.transaction.converter
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.walletconnect.model.WcBitcoinMethod
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck
@ -16,7 +17,6 @@ import com.tangem.features.walletconnect.transaction.entity.common.WcTransaction
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.core.ui.HoldToConfirmButtonFeatureToggles
import com.tangem.domain.models.wallet.isHotWallet
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
@ -27,7 +27,6 @@ internal class WcSendTransactionUMConverter @Inject constructor(
private val networkInfoUMConverter: WcNetworkInfoUMConverter,
private val requestBlockUMConverter: WcTransactionRequestBlockUMConverter,
private val notificationsFactory: WcNotificationsFactory,
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
) : Converter<WcSendTransactionUMConverter.Input, WcSendTransactionUM?> {
override fun convert(value: Input): WcSendTransactionUM? {
@ -41,6 +40,9 @@ internal class WcSendTransactionUMConverter @Inject constructor(
is WcEthMethod.SignTransaction,
is WcSolanaMethod.SignAllTransaction,
is WcSolanaMethod.SignTransaction,
is WcBitcoinMethod.SendTransfer,
is WcBitcoinMethod.SignPsbt,
is WcBitcoinMethod.SignMessage,
-> WcSendTransactionUM(
transaction = WcSendTransactionItemUM(
onDismiss = value.actions.onDismiss,
@ -65,8 +67,7 @@ internal class WcSendTransactionUMConverter @Inject constructor(
}
},
feeErrorNotification = feeErrorNotification,
isHoldToConfirmEnabled = holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled &&
value.context.session.wallet.isHotWallet,
isHoldToConfirmEnabled = value.context.session.wallet.isHotWallet,
),
feeSelectorUM = when (value.feeState) {
WcTransactionFeeState.None -> FeeSelectorUM.Loading

View file

@ -10,7 +10,6 @@ import com.tangem.features.walletconnect.transaction.entity.common.WcTransaction
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM
import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionItemUM
import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionUM
import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
import com.tangem.domain.models.wallet.isHotWallet
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
@ -20,7 +19,6 @@ internal class WcSignTransactionUMConverter @Inject constructor(
private val appInfoContentUMConverter: WcTransactionAppInfoContentUMConverter,
private val networkInfoUMConverter: WcNetworkInfoUMConverter,
private val requestBlockUMConverter: WcTransactionRequestBlockUMConverter,
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
) : Converter<WcSignTransactionUMConverter.Input, WcSignTransactionUM> {
override fun convert(value: Input) = WcSignTransactionUM(
@ -38,8 +36,7 @@ internal class WcSignTransactionUMConverter @Inject constructor(
isLoading = value.signState.domainStep == WcSignStep.Signing,
address = WcAddressConverter.convert(value.context.derivationState),
walletInteractionIcon = walletInterationIcon(value.context.session.wallet),
isHoldToConfirmEnabled = holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled &&
value.context.session.wallet.isHotWallet,
isHoldToConfirmEnabled = value.context.session.wallet.isHotWallet,
),
transactionRequestInfo = WcTransactionRequestInfoUM(
requestBlockUMConverter.convert(

View file

@ -10,7 +10,6 @@ import com.tangem.features.walletconnect.transaction.entity.common.WcTransaction
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM
import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionItemUM
import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionUM
import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
import com.tangem.domain.models.wallet.isHotWallet
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
@ -20,7 +19,6 @@ internal class WcSignTypedDataUMConverter @Inject constructor(
private val appInfoContentUMConverter: WcTransactionAppInfoContentUMConverter,
private val networkInfoUMConverter: WcNetworkInfoUMConverter,
private val requestBlockUMConverter: WcTransactionRequestBlockUMConverter,
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
) : Converter<WcSignTypedDataUMConverter.Input, WcSignTransactionUM> {
override fun convert(value: Input): WcSignTransactionUM = WcSignTransactionUM(
@ -38,8 +36,7 @@ internal class WcSignTypedDataUMConverter @Inject constructor(
address = WcAddressConverter.convert(value.context.derivationState),
isLoading = value.signState.domainStep == WcSignStep.Signing,
walletInteractionIcon = walletInterationIcon(value.context.session.wallet),
isHoldToConfirmEnabled = holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled &&
value.context.session.wallet.isHotWallet,
isHoldToConfirmEnabled = value.context.session.wallet.isHotWallet,
),
transactionRequestInfo = WcTransactionRequestInfoUM(
blocks = buildList {

View file

@ -0,0 +1,23 @@
package com.tangem.features.walletconnect.transaction.entity.addresses
import androidx.annotation.DrawableRes
import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM
/**
* UI model for Bitcoin getAccountAddresses WalletConnect request.
*/
internal data class WcGetAddressesUM(
val appInfo: WcTransactionAppInfoContentUM,
val networkInfo: WcNetworkInfoUM,
val addresses: List<AddressInfo>,
val isLoading: Boolean,
@DrawableRes val walletInteractionIcon: Int,
val onApprove: () -> Unit,
val onReject: () -> Unit,
) {
data class AddressInfo(
val address: String,
val intention: String?,
)
}

View file

@ -0,0 +1,46 @@
package com.tangem.features.walletconnect.transaction.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.domain.walletconnect.WcRequestUseCaseFactory
import com.tangem.domain.walletconnect.model.HandleMethodError
import com.tangem.domain.walletconnect.usecase.method.WcGetAddressesUseCase
import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams
import com.tangem.features.walletconnect.transaction.converter.WcHandleMethodErrorConverter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* Model for Bitcoin getAccountAddresses WalletConnect method.
*/
@Stable
@ModelScoped
internal class WcGetAddressesModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val useCaseFactory: WcRequestUseCaseFactory,
) : Model() {
private val params = paramsContainer.require<WcTransactionModelParams>()
init {
modelScope.launch {
val useCase = useCaseFactory.createUseCase<WcGetAddressesUseCase>(params.rawRequest)
.onLeft { showErrorDialog(it) }
.getOrNull() ?: return@launch
useCase.invoke()
.onLeft { showErrorDialog(it) }
.onRight { router.pop() }
}
}
private fun showErrorDialog(error: HandleMethodError) {
router.push(WcHandleMethodErrorConverter.convert(error))
}
}

View file

@ -35,6 +35,8 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import com.tangem.utils.logging.TangemLogger
import com.tangem.domain.walletconnect.WC_TAG
import javax.inject.Inject
import kotlin.properties.Delegates
@ -67,14 +69,36 @@ internal class WcSignTransactionModel @Inject constructor(
init {
modelScope.launch {
TangemLogger.withTag(WC_TAG).i("Creating use case...")
useCase = useCaseFactory.createUseCase<WcMessageSignUseCase>(params.rawRequest)
.onLeft { router.push(WcHandleMethodErrorConverter.convert(it)) }
.getOrNull() ?: return@launch
.onLeft { error ->
TangemLogger.withTag(WC_TAG).e("Failed to create use case: $error")
router.push(WcHandleMethodErrorConverter.convert(error))
}
.getOrNull() ?: run {
TangemLogger.withTag(WC_TAG).e("Use case is null, exiting")
return@launch
}
TangemLogger.withTag(WC_TAG).i("Use case created successfully")
TangemLogger.withTag(WC_TAG).i("Use case type: ${useCase.javaClass.simpleName}")
TangemLogger.withTag(WC_TAG).i("Method: ${useCase.method}")
sendSignatureReceivedAnalytics(useCase)
TangemLogger.withTag(WC_TAG).i("Invoking use case...")
useCase.invoke()
.onEach { signState ->
if (signingIsDone(signState)) return@onEach
TangemLogger.withTag(WC_TAG).i("Sign state received: ${signState.javaClass.simpleName}")
if (signingIsDone(signState)) {
TangemLogger.withTag(WC_TAG).i("Signing is DONE, not updating UI")
return@onEach
}
TangemLogger.withTag(WC_TAG).i("Converting to UI state...")
val signTransactionUM = convertToUI(useCase, signState)
TangemLogger.withTag(WC_TAG).i("UI state created, emitting...")
_uiState.emit(signTransactionUM)
}
.launchIn(this)
@ -101,7 +125,10 @@ internal class WcSignTransactionModel @Inject constructor(
portfolioName = portfolioNameDelegate.createAccountTitleUM(useCase.session),
),
)
is WcEthMethod.MessageSign, is WcSolanaMethod.SignMessage -> signTransactionUMConverter.convert(
is WcEthMethod.MessageSign,
is WcSolanaMethod.SignMessage,
is com.tangem.domain.walletconnect.model.WcBitcoinMethod.SignMessage,
-> signTransactionUMConverter.convert(
WcSignTransactionUMConverter.Input(
context = useCase,
signState = signState,
@ -110,7 +137,10 @@ internal class WcSignTransactionModel @Inject constructor(
portfolioName = portfolioNameDelegate.createAccountTitleUM(useCase.session),
),
)
else -> null
else -> {
TangemLogger.withTag(WC_TAG).e("UNSUPPORTED METHOD: ${useCase.method.javaClass.simpleName}")
null
}
}
}