diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletConnectDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletConnectDomainModule.kt
index fd5d2491d3..780bea965e 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/WalletConnectDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/WalletConnectDomainModule.kt
@@ -1,6 +1,13 @@
package com.tangem.tap.di.domain
+import com.tangem.blockchain.common.TransactionSigner
+import com.tangem.data.wallets.hot.TangemHotWalletSigner
+import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins
+import com.tangem.domain.card.models.TwinKey
+import com.tangem.domain.card.repository.CardSdkConfigRepository
+import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
+import com.tangem.domain.walletconnect.WcTransactionSignerProvider
import com.tangem.domain.walletconnect.repository.WalletConnectRepository
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.walletconnect.usecase.WcSessionsUseCase
@@ -27,4 +34,27 @@ internal object WalletConnectDomainModule {
fun providesWcSessionsUseCase(sessionsManager: WcSessionsManager): WcSessionsUseCase {
return WcSessionsUseCase(sessionsManager)
}
+
+ @Provides
+ @Singleton
+ fun providesWcTransactionSignerProvider(
+ cardSdkConfigRepository: CardSdkConfigRepository,
+ tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
+ ): WcTransactionSignerProvider {
+ return object : WcTransactionSignerProvider {
+ override fun createSigner(wallet: UserWallet): TransactionSigner {
+ return when (wallet) {
+ is UserWallet.Hot -> tangemHotWalletSignerFactory.create(wallet)
+ is UserWallet.Cold -> {
+ val card = wallet.scanResponse.card
+ val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins
+ cardSdkConfigRepository.getCommonSigner(
+ cardId = card.cardId.takeIf { isCardNotBackedUp },
+ twinKey = TwinKey.getOrNull(scanResponse = wallet.scanResponse),
+ )
+ }
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json
index 214bc42fee..37b1b10533 100644
--- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json
+++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json
@@ -59,5 +59,9 @@
{
"name": "ADD_AND_MANAGE_TOKENS_ENABLED",
"version": "undefined"
+ },
+ {
+ "name": "WALLET_CONNECT_BITCOIN_ENABLED",
+ "version": "undefined"
}
]
diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index 4f79cdf591..6321abad93 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -2150,6 +2150,8 @@
At least one network is required for dApp connection
Specify selected networks
Successfully signed
+ Share Addresses
+ Addresses to share
WalletConnect
To
Transaction request
diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt
index 59cf796d98..a5da6c9298 100644
--- a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt
+++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt
@@ -33,6 +33,7 @@ internal class DefaultBlockAidRepository(
return when (data.params) {
is TransactionParams.Evm -> scanEvmTransaction(data = data)
is TransactionParams.Solana -> scanSolanaTransaction(data = data)
+ is TransactionParams.Bitcoin -> scanBitcoinTransaction(data = data)
}
}
@@ -59,6 +60,17 @@ internal class DefaultBlockAidRepository(
mapper.mapToDomain(response)
}
+ @Suppress("UnusedParameter")
+ private fun scanBitcoinTransaction(data: TransactionData): CheckTransactionResult {
+ // TODO: BlockAid API doesn't support Bitcoin transaction scanning yet
+ // When support is added, implement: api.scanBitcoinTransaction(mapper.mapToBitcoinRequest(data))
+ return CheckTransactionResult(
+ validation = com.domain.blockaid.models.transaction.ValidationResult.FAILED_TO_VALIDATE,
+ description = "Bitcoin transaction validation is not yet supported by BlockAid",
+ simulation = com.domain.blockaid.models.transaction.SimulationResult.FailedToSimulate,
+ )
+ }
+
private suspend fun scanEvmTransactionBulk(
blockchain: Blockchain,
transactionDataList: List,
diff --git a/data/wallet-connect/build.gradle.kts b/data/wallet-connect/build.gradle.kts
index b7fd3d93a1..46be3e797d 100644
--- a/data/wallet-connect/build.gradle.kts
+++ b/data/wallet-connect/build.gradle.kts
@@ -34,6 +34,7 @@ dependencies {
/* Project - Core */
implementation(projects.core.utils)
implementation(projects.core.analytics)
+ api(projects.core.configToggles)
/* DI */
implementation(deps.hilt.core)
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt
index dc5c0bb1ba..59d3fc8014 100644
--- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt
@@ -6,6 +6,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.DefaultWalletConnectRepository
import com.tangem.data.walletconnect.initialize.DefaultWcInitializeUseCase
+import com.tangem.data.walletconnect.network.bitcoin.WcBitcoinNetwork
import com.tangem.data.walletconnect.network.ethereum.WcEthNetwork
import com.tangem.data.walletconnect.network.solana.WcSolanaNetwork
import com.tangem.data.walletconnect.pair.*
@@ -25,6 +26,7 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.WcRequestService
import com.tangem.domain.walletconnect.WcRequestUseCaseFactory
+import com.tangem.domain.walletconnect.featuretoggle.WalletConnectFeatureToggles
import com.tangem.domain.walletconnect.repository.WalletConnectRepository
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.walletconnect.usecase.disconnect.WcDisconnectUseCase
@@ -34,6 +36,8 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
+import com.tangem.core.configtoggle.feature.FeatureTogglesManager
+import com.tangem.data.walletconnect.featuretoggle.DefaultWalletConnectFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -162,6 +166,22 @@ internal object WalletConnectDataModule {
networksConverter = wcNetworksConverter,
)
+ @Provides
+ @Singleton
+ fun wcBitcoinNetwork(
+ @SdkMoshi moshi: Moshi,
+ wcNetworksConverter: WcNetworksConverter,
+ sessionsManager: WcSessionsManager,
+ factories: WcBitcoinNetwork.Factories,
+ walletManagersFacade: WalletManagersFacade,
+ ): WcBitcoinNetwork = WcBitcoinNetwork(
+ moshi = moshi,
+ sessionsManager = sessionsManager,
+ factories = factories,
+ networksConverter = wcNetworksConverter,
+ walletManagersFacade = walletManagersFacade,
+ )
+
@Provides
@Singleton
fun caipNamespaceDelegate(
@@ -198,11 +218,19 @@ internal object WalletConnectDataModule {
@Provides
@Singleton
- fun diHelperBox(ethNetwork: WcEthNetwork, solanaNetwork: WcSolanaNetwork) = DiHelperBox(
- handlers = setOf(
- ethNetwork,
- solanaNetwork,
- ),
+ fun diHelperBox(
+ ethNetwork: WcEthNetwork,
+ solanaNetwork: WcSolanaNetwork,
+ bitcoinNetwork: WcBitcoinNetwork,
+ featureToggles: WalletConnectFeatureToggles,
+ ) = DiHelperBox(
+ handlers = buildSet {
+ add(ethNetwork)
+ add(solanaNetwork)
+ if (featureToggles.isBitcoinEnabled) {
+ add(bitcoinNetwork)
+ }
+ },
)
@Provides
@@ -210,10 +238,15 @@ internal object WalletConnectDataModule {
fun namespaceConverters(
ethNamespaceConverter: WcEthNetwork.NamespaceConverter,
solanaNamespaceConverter: WcSolanaNetwork.NamespaceConverter,
- ): Set<@JvmSuppressWildcards WcNamespaceConverter> = setOf(
- ethNamespaceConverter,
- solanaNamespaceConverter,
- )
+ bitcoinNamespaceConverter: WcBitcoinNetwork.NamespaceConverter,
+ featureToggles: WalletConnectFeatureToggles,
+ ): Set<@JvmSuppressWildcards WcNamespaceConverter> = buildSet {
+ add(ethNamespaceConverter)
+ add(solanaNamespaceConverter)
+ if (featureToggles.isBitcoinEnabled) {
+ add(bitcoinNamespaceConverter)
+ }
+ }
@Provides
@Singleton
@@ -239,6 +272,14 @@ internal object WalletConnectDataModule {
return WcSolanaNetwork.NamespaceConverter(excludedBlockchains)
}
+ @Provides
+ @Singleton
+ fun wcBitcoinNetworkNamespaceConverter(
+ excludedBlockchains: ExcludedBlockchains,
+ ): WcBitcoinNetwork.NamespaceConverter {
+ return WcBitcoinNetwork.NamespaceConverter(excludedBlockchains)
+ }
+
@Provides
@Singleton
fun providesWcDisconnectUseCase(
@@ -248,6 +289,14 @@ internal object WalletConnectDataModule {
return WcDisconnectUseCase(sessionsManager, analytics)
}
+ @Provides
+ @Singleton
+ fun providesWalletConnectFeatureToggles(
+ featureTogglesManager: FeatureTogglesManager,
+ ): WalletConnectFeatureToggles {
+ return DefaultWalletConnectFeatureToggles(featureTogglesManager)
+ }
+
internal class DiHelperBox(
val handlers: Set,
)
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/featuretoggle/DefaultWalletConnectFeatureToggles.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/featuretoggle/DefaultWalletConnectFeatureToggles.kt
new file mode 100644
index 0000000000..1f8d8184a4
--- /dev/null
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/featuretoggle/DefaultWalletConnectFeatureToggles.kt
@@ -0,0 +1,13 @@
+package com.tangem.data.walletconnect.featuretoggle
+
+import com.tangem.core.configtoggle.FeatureToggles
+import com.tangem.core.configtoggle.feature.FeatureTogglesManager
+import com.tangem.domain.walletconnect.featuretoggle.WalletConnectFeatureToggles
+
+internal class DefaultWalletConnectFeatureToggles(
+ private val featureTogglesManager: FeatureTogglesManager,
+) : WalletConnectFeatureToggles {
+
+ override val isBitcoinEnabled: Boolean
+ get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.WALLET_CONNECT_BITCOIN_ENABLED)
+}
\ No newline at end of file
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt
index 13522ffe02..00dab5a545 100644
--- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt
@@ -9,7 +9,7 @@ import com.reown.walletkit.client.WalletKit
import com.tangem.data.walletconnect.pair.WcPairSdkDelegate
import com.tangem.data.walletconnect.request.DefaultWcRequestService
import com.tangem.data.walletconnect.sessions.DefaultWcSessionsManager
-import com.tangem.data.walletconnect.utils.WC_TAG
+import com.tangem.domain.walletconnect.WC_TAG
import com.tangem.data.walletconnect.utils.WcSdkObserver
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.utils.logging.TangemLogger
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/Model.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/Model.kt
new file mode 100644
index 0000000000..2cabcd17c7
--- /dev/null
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/Model.kt
@@ -0,0 +1,112 @@
+package com.tangem.data.walletconnect.network.bitcoin
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+/**
+ * JSON request model for sendTransfer method.
+ */
+@JsonClass(generateAdapter = true)
+internal data class WcBitcoinSendTransferRequest(
+ @Json(name = "account")
+ val account: String,
+
+ @Json(name = "recipientAddress")
+ val recipientAddress: String,
+
+ @Json(name = "amount")
+ val amount: String,
+
+ @Json(name = "memo")
+ val memo: String? = null,
+
+ @Json(name = "changeAddress")
+ val changeAddress: String? = null,
+)
+
+/**
+ * JSON request model for getAccountAddresses method.
+ */
+@JsonClass(generateAdapter = true)
+internal data class WcBitcoinGetAccountAddressesRequest(
+ @Json(name = "account")
+ val account: String? = null,
+
+ @Json(name = "intentions")
+ val intentions: List? = null,
+)
+
+/**
+ * JSON request model for signPsbt method.
+ */
+@JsonClass(generateAdapter = true)
+internal data class WcBitcoinSignPsbtRequest(
+ @Json(name = "psbt")
+ val psbt: String,
+
+ @Json(name = "signInputs")
+ val signInputs: List,
+
+ @Json(name = "broadcast")
+ val isBroadcast: Boolean? = false,
+)
+
+/**
+ * JSON model for sign input specification.
+ */
+@JsonClass(generateAdapter = true)
+internal data class WcBitcoinSignInput(
+ @Json(name = "address")
+ val address: String,
+
+ @Json(name = "index")
+ val index: Int,
+
+ @Json(name = "sighashTypes")
+ val sighashTypes: List? = null,
+)
+
+/**
+ * JSON request model for signMessage method.
+ */
+@JsonClass(generateAdapter = true)
+internal data class WcBitcoinSignMessageRequest(
+ @Json(name = "account")
+ val account: String,
+
+ @Json(name = "message")
+ val message: String,
+
+ @Json(name = "address")
+ val address: String? = null,
+
+ @Json(name = "protocol")
+ val protocol: String? = "ecdsa",
+)
+
+/**
+ * JSON response model for getAccountAddresses method.
+ */
+@JsonClass(generateAdapter = true)
+internal data class WcBitcoinGetAccountAddressesResponse(
+ @Json(name = "addresses")
+ val addresses: List,
+)
+
+/**
+ * Address information in getAccountAddresses response.
+ */
+@JsonClass(generateAdapter = true)
+internal data class WcBitcoinAddressInfo(
+ @Json(name = "address")
+ val address: String,
+
+ @Json(name = "publicKey")
+ val publicKey: String? = null,
+
+ @Json(name = "path")
+ val path: String? = null,
+
+ @Json(name = "intention")
+ val intention: String? = null,
+)
\ No newline at end of file
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/WcBitcoinGetAccountAddressesUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/WcBitcoinGetAccountAddressesUseCase.kt
new file mode 100644
index 0000000000..8b182c1263
--- /dev/null
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/WcBitcoinGetAccountAddressesUseCase.kt
@@ -0,0 +1,118 @@
+package com.tangem.data.walletconnect.network.bitcoin
+
+import arrow.core.Either
+import arrow.core.left
+import arrow.core.right
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import com.squareup.moshi.Moshi
+import com.tangem.blockchain.blockchains.bitcoin.walletconnect.models.AccountAddress
+import com.tangem.blockchain.extensions.Result as SdkResult
+import com.tangem.data.walletconnect.respond.WcRespondService
+import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
+import com.tangem.datasource.di.SdkMoshi
+import com.tangem.domain.models.network.Network
+import com.tangem.domain.walletconnect.model.HandleMethodError
+import com.tangem.domain.walletconnect.model.WcBitcoinMethod
+import com.tangem.domain.walletconnect.model.WcSession
+import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
+import com.tangem.domain.walletconnect.usecase.method.WcGetAddressesUseCase
+import com.tangem.domain.walletconnect.usecase.method.WcNetworkDerivationState
+import com.tangem.domain.walletmanager.WalletManagersFacade
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/**
+ * Use case for Bitcoin getAccountAddresses WalletConnect method.
+ *
+ * Returns wallet addresses filtered by intention (payment/ordinal).
+ * This is a non-signing operation.
+ */
+@JsonClass(generateAdapter = true)
+internal data class AddressInfo(
+ @Json(name = "address") val address: String,
+ @Json(name = "publicKey") val publicKey: String? = null,
+ @Json(name = "path") val path: String? = null,
+ @Json(name = "intention") val intention: String? = null,
+)
+
+internal class WcBitcoinGetAccountAddressesUseCase @AssistedInject constructor(
+ @Assisted val context: WcMethodUseCaseContext,
+ @Assisted override val method: WcBitcoinMethod.GetAccountAddresses,
+ private val walletManagersFacade: WalletManagersFacade,
+ private val respondService: WcRespondService,
+ @SdkMoshi private val moshi: Moshi,
+) : WcGetAddressesUseCase {
+
+ override val wallet get() = context.session.wallet
+
+ override val session: WcSession
+ get() = context.session
+ override val rawSdkRequest: WcSdkSessionRequest
+ get() = context.rawSdkRequest
+ override val network: Network
+ get() = context.network
+ override val derivationState: WcNetworkDerivationState = when {
+ context.networkDerivationsCount > 1 -> WcNetworkDerivationState.Multiple(walletAddress = context.accountAddress)
+ else -> WcNetworkDerivationState.Single
+ }
+
+ override suspend fun invoke(): Either {
+ val walletManager = walletManagersFacade.getOrCreateWalletManager(wallet.walletId, network)
+ ?: return HandleMethodError.UnknownError("Failed to create wallet manager").left()
+ return when (val result = walletManager.getAddresses(filterOptions = method.intentions)) {
+ is SdkResult.Success -> {
+ val accountAddresses = result.data.map { addressInfo ->
+ AccountAddress(
+ address = addressInfo.address,
+ publicKey = addressInfo.publicKey,
+ path = addressInfo.derivationPath,
+ intention = addressInfo.metadata?.get("intention") as? String,
+ )
+ }
+ val response = buildJsonResponse(accountAddresses)
+ respondService.respond(rawSdkRequest, response)
+ WcGetAddressesUseCase.GetAddressesResult(
+ addresses = accountAddresses.map { addr ->
+ WcGetAddressesUseCase.AddressInfo(
+ address = addr.address,
+ publicKey = addr.publicKey,
+ path = addr.path,
+ intention = addr.intention,
+ )
+ },
+ ).right()
+ }
+ is SdkResult.Failure -> {
+ HandleMethodError.UnknownError(result.error.customMessage).left()
+ }
+ }
+ }
+
+ override fun reject() {
+ respondService.rejectRequestNonBlock(rawSdkRequest)
+ }
+
+ private fun buildJsonResponse(accountAddresses: List): String {
+ val addresses = accountAddresses.map { addr ->
+ AddressInfo(
+ address = addr.address,
+ publicKey = addr.publicKey,
+ path = addr.path,
+ intention = addr.intention,
+ )
+ }
+ return moshi.adapter>(
+ com.squareup.moshi.Types.newParameterizedType(List::class.java, AddressInfo::class.java),
+ ).toJson(addresses)
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ context: WcMethodUseCaseContext,
+ method: WcBitcoinMethod.GetAccountAddresses,
+ ): WcBitcoinGetAccountAddressesUseCase
+ }
+}
\ No newline at end of file
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/WcBitcoinNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/WcBitcoinNetwork.kt
new file mode 100644
index 0000000000..38c35fbeed
--- /dev/null
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/WcBitcoinNetwork.kt
@@ -0,0 +1,214 @@
+package com.tangem.data.walletconnect.network.bitcoin
+
+import arrow.core.Either
+import arrow.core.getOrElse
+import arrow.core.left
+import arrow.core.right
+import com.squareup.moshi.Moshi
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.blockchainsdk.utils.ExcludedBlockchains
+import com.tangem.data.walletconnect.model.CAIP2
+import com.tangem.data.walletconnect.model.NamespaceKey
+import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter
+import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter.Companion.fromJson
+import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
+import com.tangem.data.walletconnect.utils.WcNamespaceConverter
+import com.tangem.data.walletconnect.utils.WcNetworksConverter
+import com.tangem.domain.walletconnect.model.HandleMethodError
+import com.tangem.domain.walletconnect.model.WcBitcoinMethod
+import com.tangem.domain.walletconnect.model.WcBitcoinMethodName
+import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
+import com.tangem.domain.walletconnect.repository.WcSessionsManager
+import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase
+import com.tangem.domain.walletmanager.WalletManagersFacade
+import jakarta.inject.Inject
+
+/**
+ * WalletConnect request handler for Bitcoin blockchain.
+ *
+ * Handles Bitcoin-specific RPC methods: sendTransfer, getAccountAddresses, signPsbt, signMessage.
+ *
+ * @see Bitcoin RPC Reference
+ */
+internal class WcBitcoinNetwork(
+ private val moshi: Moshi,
+ private val sessionsManager: WcSessionsManager,
+ private val factories: Factories,
+ private val networksConverter: WcNetworksConverter,
+ private val walletManagersFacade: WalletManagersFacade,
+) : WcRequestToUseCaseConverter {
+
+ override fun toWcMethodName(request: WcSdkSessionRequest): WcBitcoinMethodName? {
+ val methodKey = request.request.method
+ return WcBitcoinMethodName.entries.find { it.raw == methodKey }
+ }
+
+ @Suppress("CyclomaticComplexMethod")
+ override suspend fun toUseCase(request: WcSdkSessionRequest): Either {
+ fun error(message: String) = HandleMethodError.UnknownError(message).left()
+
+ val name = toWcMethodName(request) ?: return error("Unknown method name")
+ val method: WcBitcoinMethod = name.toMethod(request)
+ .getOrElse { return error(it.message.orEmpty()) }
+ ?: return error("Failed to parse $name")
+
+ val session = sessionsManager.findSessionByTopic(request.topic)
+ ?: return HandleMethodError.UnknownSession.left()
+
+ val wallet = session.wallet
+ val chainId = request.chainId.orEmpty()
+
+ val account = session.account
+
+ suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(
+ rawChainId = chainId,
+ account = account,
+ )
+
+ suspend fun anyAddress() = anyExistNetwork()
+ ?.let { network -> walletManagersFacade.getDefaultAddress(wallet.walletId, network).orEmpty() }
+ .orEmpty()
+
+ val accountAddress = when (method) {
+ is WcBitcoinMethod.SendTransfer -> method.account
+ is WcBitcoinMethod.GetAccountAddresses -> method.account
+ is WcBitcoinMethod.SignPsbt -> method.signInputs.firstOrNull()?.address ?: anyAddress()
+ is WcBitcoinMethod.SignMessage -> method.address ?: method.account
+ }
+
+ val walletNetwork = networksConverter
+ .findWalletNetworkForRequest(request, session, accountAddress)
+ ?: anyExistNetwork()
+ ?: return error("Failed to find walletNetwork for accountAddress $accountAddress")
+
+ val context = WcMethodUseCaseContext(
+ session = session,
+ rawSdkRequest = request,
+ network = walletNetwork,
+ accountAddress = accountAddress,
+ networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(
+ rawChainId = chainId,
+ account = account,
+ ).size,
+ )
+
+ val useCase = when (method) {
+ is WcBitcoinMethod.SendTransfer -> factories.sendTransfer.create(context, method)
+ is WcBitcoinMethod.GetAccountAddresses -> factories.getAccountAddresses.create(context, method)
+ is WcBitcoinMethod.SignPsbt -> factories.signPsbt.create(context, method)
+ is WcBitcoinMethod.SignMessage -> factories.signMessage.create(context, method)
+ }
+ return useCase.right()
+ }
+
+ private fun WcBitcoinMethodName.toMethod(request: WcSdkSessionRequest): Either {
+ val rawParams = request.request.params
+ return when (this) {
+ WcBitcoinMethodName.SendTransfer -> moshi.fromJson(rawParams)
+ .getOrElse { return it.left() }
+ ?.let { req ->
+ WcBitcoinMethod.SendTransfer(
+ account = req.account,
+ recipientAddress = req.recipientAddress,
+ amount = req.amount,
+ memo = req.memo,
+ changeAddress = req.changeAddress,
+ )
+ }
+ WcBitcoinMethodName.GetAccountAddresses -> moshi.fromJson(rawParams)
+ .getOrElse { return it.left() }
+ ?.let { req ->
+ WcBitcoinMethod.GetAccountAddresses(
+ account = req.account.orEmpty(),
+ intentions = req.intentions,
+ )
+ }
+ WcBitcoinMethodName.SignPsbt -> moshi.fromJson(rawParams)
+ .getOrElse { return it.left() }
+ ?.let { req ->
+ WcBitcoinMethod.SignPsbt(
+ psbt = req.psbt,
+ signInputs = req.signInputs.map { input ->
+ WcBitcoinMethod.SignInput(
+ address = input.address,
+ index = input.index,
+ sighashTypes = input.sighashTypes,
+ )
+ },
+ shouldBroadcast = req.isBroadcast == true,
+ )
+ }
+ WcBitcoinMethodName.SignMessage -> moshi.fromJson(rawParams)
+ .getOrElse { return it.left() }
+ ?.let { req ->
+ WcBitcoinMethod.SignMessage(
+ account = req.account,
+ message = req.message,
+ address = req.address,
+ protocol = req.protocol ?: "ecdsa",
+ )
+ }
+ }.right()
+ }
+
+ /**
+ * Bitcoin namespace converter for CAIP-2 chain IDs.
+ *
+ * Bitcoin uses BIP-122 namespace with genesis block hash as reference.
+ * Example: bip122:000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
+ */
+ internal class NamespaceConverter @Inject constructor(
+ override val excludedBlockchains: ExcludedBlockchains,
+ ) : WcNamespaceConverter {
+
+ override val namespaceKey: NamespaceKey = NamespaceKey(NAMESPACE)
+
+ override fun toBlockchain(chainId: CAIP2): Blockchain? {
+ if (chainId.namespace != namespaceKey.key) return null
+ return when {
+ isMainnetReference(chainId.reference) -> Blockchain.Bitcoin
+ isTestnetReference(chainId.reference) -> Blockchain.BitcoinTestnet
+ else -> null
+ }
+ }
+
+ private fun isMainnetReference(reference: String): Boolean {
+ return MAINNET_GENESIS_PREFIX.any { reference.startsWith(it, ignoreCase = true) } ||
+ reference.equals("mainnet", ignoreCase = true)
+ }
+
+ private fun isTestnetReference(reference: String): Boolean {
+ return TESTNET_GENESIS_PREFIX.any { reference.startsWith(it, ignoreCase = true) } ||
+ reference.equals("testnet", ignoreCase = true)
+ }
+
+ companion object {
+ private const val NAMESPACE = "bip122"
+
+ // Bitcoin mainnet genesis hash prefixes (supports any truncated version)
+ private val MAINNET_GENESIS_PREFIX = listOf(
+ "000000000019d6689c085ae165831e93", // Mainnet genesis hash prefix (min 32 chars for uniqueness)
+ )
+
+ // Bitcoin testnet genesis hash prefixes (supports any truncated version)
+ private val TESTNET_GENESIS_PREFIX = listOf(
+ "000000000933ea01ad0ee984209779ba", // Standard testnet genesis hash prefix (9 leading zeros)
+ "0000000000933ea01ad0ee984209779ba", // Alternative testnet prefix (10 leading zeros)
+ )
+ }
+ }
+
+ /**
+ * Factory classes for creating Bitcoin WalletConnect use cases.
+ */
+ internal class Factories @Inject constructor(
+ val sendTransfer: WcBitcoinSendTransferUseCase.Factory,
+ val getAccountAddresses: WcBitcoinGetAccountAddressesUseCase.Factory,
+ val signPsbt: WcBitcoinSignPsbtUseCase.Factory,
+ val signMessage: WcBitcoinSignMessageUseCase.Factory,
+ )
+
+ companion object {
+ private const val NAMESPACE = "bip122"
+ }
+}
\ No newline at end of file
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/WcBitcoinSendTransferUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/WcBitcoinSendTransferUseCase.kt
new file mode 100644
index 0000000000..0ded51f956
--- /dev/null
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/WcBitcoinSendTransferUseCase.kt
@@ -0,0 +1,146 @@
+package com.tangem.data.walletconnect.network.bitcoin
+
+import arrow.core.left
+import com.tangem.blockchain.blockchains.bitcoin.BitcoinTransactionExtras
+import com.tangem.blockchain.common.Amount
+import com.tangem.blockchain.common.TransactionData
+import com.tangem.blockchain.common.transaction.Fee
+import com.tangem.blockchain.extensions.Result as SdkResult
+import com.tangem.core.analytics.api.AnalyticsEventHandler
+import com.tangem.data.walletconnect.respond.WcRespondService
+import com.tangem.data.walletconnect.sign.BaseWcSignUseCase
+import com.tangem.data.walletconnect.sign.SignCollector
+import com.tangem.data.walletconnect.sign.SignStateConverter.toResult
+import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
+import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate
+import com.tangem.domain.core.lce.LceFlow
+import com.tangem.domain.walletconnect.WcTransactionSignerProvider
+import com.tangem.domain.walletconnect.model.HandleMethodError
+import com.tangem.domain.walletconnect.model.WcBitcoinMethod
+import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck
+import com.tangem.domain.walletconnect.usecase.method.WcMutableFee
+import com.tangem.domain.walletconnect.usecase.method.WcSignState
+import com.tangem.domain.walletconnect.usecase.method.WcTransactionUseCase
+import com.tangem.domain.walletmanager.WalletManagersFacade
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.FlowCollector
+import kotlinx.coroutines.flow.emitAll
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
+import java.math.BigDecimal
+
+/**
+ * Use case for Bitcoin sendTransfer WalletConnect method.
+ *
+ * Sends a Bitcoin transfer transaction with optional memo (OP_RETURN) and custom change address.
+ */
+@Suppress("LongParameterList")
+internal class WcBitcoinSendTransferUseCase @AssistedInject constructor(
+ @Assisted override val context: WcMethodUseCaseContext,
+ @Assisted override val method: WcBitcoinMethod.SendTransfer,
+ private val walletManagersFacade: WalletManagersFacade,
+ private val signerProvider: WcTransactionSignerProvider,
+ override val respondService: WcRespondService,
+ override val analytics: AnalyticsEventHandler,
+ blockAidDelegate: BlockAidVerificationDelegate,
+) : BaseWcSignUseCase(),
+ WcTransactionUseCase,
+ WcMutableFee {
+
+ override val wallet get() = context.session.wallet
+
+ private val transferAmount: Amount by lazy {
+ createAmountFromSatoshis(method.amount)
+ }
+
+ override val securityStatus: LceFlow =
+ blockAidDelegate.getSecurityStatus(
+ network = network,
+ method = method,
+ rawSdkRequest = rawSdkRequest,
+ session = session,
+ accountAddress = context.accountAddress,
+ ).map { lce ->
+ lce.map { result -> BlockAidTransactionCheck.Result.Plain(result) }
+ }
+
+ override suspend fun SignCollector.onSign(state: WcSignState) {
+ val walletManager = walletManagersFacade.getOrCreateWalletManager(wallet.walletId, network)
+ ?: run {
+ emit(state.toResult(HandleMethodError.UnknownError("Failed to create wallet manager").left()))
+ return
+ }
+
+ val signer = signerProvider.createSigner(wallet)
+ when (val result = walletManager.send(state.signModel, signer)) {
+ is SdkResult.Success -> {
+ val response = buildJsonResponse(result.data.hash)
+ val wcRespondResult = respondService.respond(rawSdkRequest, response)
+ emit(state.toResult(wcRespondResult))
+ }
+ is SdkResult.Failure -> {
+ emit(state.toResult(HandleMethodError.UnknownError(result.error.customMessage).left()))
+ }
+ }
+ }
+
+ override suspend fun FlowCollector.onMiddleAction(
+ signModel: TransactionData,
+ action: WcBitcoinTxAction,
+ ) {
+ val uncompiled = signModel as? TransactionData.Uncompiled ?: return
+ val newState = when (action) {
+ is WcBitcoinTxAction.UpdateFee -> uncompiled.copy(fee = action.fee)
+ }
+ emit(newState)
+ }
+
+ override suspend fun dAppFee(): Fee? = null
+
+ override fun updateFee(fee: Fee) {
+ middleAction(WcBitcoinTxAction.UpdateFee(fee))
+ }
+
+ override fun invoke(): Flow> = flow {
+ val fee = dAppFee()
+ val transactionData = createTransactionData(fee)
+ emitAll(delegate.invoke(transactionData))
+ }
+
+ private fun createTransactionData(fee: Fee?): TransactionData.Uncompiled {
+ return TransactionData.Uncompiled(
+ amount = transferAmount,
+ fee = fee,
+ sourceAddress = context.accountAddress,
+ destinationAddress = method.recipientAddress,
+ extras = BitcoinTransactionExtras(
+ memo = method.memo,
+ changeAddress = method.changeAddress,
+ ),
+ )
+ }
+
+ private fun createAmountFromSatoshis(satoshis: String): Amount {
+ val btcValue = BigDecimal(satoshis).divide(SATOSHI_IN_BTC)
+ return Amount(
+ currencySymbol = network.currencySymbol,
+ value = btcValue,
+ decimals = BITCOIN_DECIMALS,
+ )
+ }
+
+ private fun buildJsonResponse(txid: String): String = "{\"txid\":\"$txid\"}"
+
+ @AssistedFactory
+ interface Factory {
+ fun create(context: WcMethodUseCaseContext, method: WcBitcoinMethod.SendTransfer): WcBitcoinSendTransferUseCase
+ }
+
+ private companion object {
+ val SATOSHI_IN_BTC = BigDecimal("100000000")
+ const val BITCOIN_DECIMALS = 8
+ }
+}
\ No newline at end of file
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/WcBitcoinSignMessageUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/WcBitcoinSignMessageUseCase.kt
new file mode 100644
index 0000000000..0f60af6c1e
--- /dev/null
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/WcBitcoinSignMessageUseCase.kt
@@ -0,0 +1,117 @@
+package com.tangem.data.walletconnect.network.bitcoin
+
+import arrow.core.left
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import com.squareup.moshi.Moshi
+import com.tangem.blockchain.extensions.Result as SdkResult
+import com.tangem.core.analytics.api.AnalyticsEventHandler
+import com.tangem.data.walletconnect.respond.WcRespondService
+import com.tangem.data.walletconnect.sign.BaseWcSignUseCase
+import com.tangem.data.walletconnect.sign.SignCollector
+import com.tangem.data.walletconnect.sign.SignStateConverter.toResult
+import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
+import com.tangem.datasource.di.SdkMoshi
+import com.domain.blockaid.models.transaction.CheckTransactionResult
+import com.domain.blockaid.models.transaction.SimulationResult
+import com.domain.blockaid.models.transaction.ValidationResult
+import com.tangem.domain.core.lce.Lce
+import com.tangem.domain.core.lce.LceFlow
+import com.tangem.domain.walletconnect.WcTransactionSignerProvider
+import com.tangem.domain.walletconnect.model.HandleMethodError
+import com.tangem.domain.walletconnect.model.WcBitcoinMethod
+import com.tangem.domain.walletconnect.usecase.method.WcMessageSignUseCase
+import com.tangem.domain.walletconnect.usecase.method.WcSignState
+import com.tangem.domain.walletmanager.WalletManagersFacade
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
+/**
+ * Use case for Bitcoin signMessage WalletConnect method.
+ *
+ * Signs an arbitrary message using Bitcoin message signing format (BIP-137 ECDSA).
+ */
+@JsonClass(generateAdapter = true)
+internal data class SignMessageResponse(
+ @Json(name = "address") val address: String,
+ @Json(name = "signature") val signature: String,
+ @Json(name = "messageHash") val messageHash: String? = null,
+)
+
+@Suppress("LongParameterList")
+internal class WcBitcoinSignMessageUseCase @AssistedInject constructor(
+ @Assisted override val context: WcMethodUseCaseContext,
+ @Assisted override val method: WcBitcoinMethod.SignMessage,
+ private val walletManagersFacade: WalletManagersFacade,
+ private val signerProvider: WcTransactionSignerProvider,
+ override val respondService: WcRespondService,
+ override val analytics: AnalyticsEventHandler,
+ @SdkMoshi private val moshi: Moshi,
+) : BaseWcSignUseCase(),
+ WcMessageSignUseCase {
+
+ override val wallet get() = context.session.wallet
+
+ // BlockAid doesn't support Bitcoin message signing
+ override val securityStatus: LceFlow = flowOf(
+ Lce.Content(
+ CheckTransactionResult(
+ validation = ValidationResult.FAILED_TO_VALIDATE,
+ simulation = SimulationResult.FailedToSimulate,
+ ),
+ ),
+ )
+
+ override suspend fun SignCollector.onSign(
+ state: WcSignState,
+ ) {
+ val walletManager = walletManagersFacade.getOrCreateWalletManager(wallet.walletId, network)
+ ?: run {
+ emit(state.toResult(HandleMethodError.UnknownError("Failed to create wallet manager").left()))
+ return
+ }
+
+ val signer = signerProvider.createSigner(wallet)
+
+ // Use the address from method, or fallback to account if not specified
+ val addressToSign = method.address ?: method.account
+
+ // Use MessageSigner to sign the message
+ when (val result = walletManager.signMessage(
+ message = method.message,
+ address = addressToSign,
+ protocol = method.protocol,
+ signer = signer,
+ )) {
+ is SdkResult.Success -> {
+ val response = buildJsonResponse(result.data)
+ val wcRespondResult = respondService.respond(rawSdkRequest, response)
+ emit(state.toResult(wcRespondResult))
+ }
+ is SdkResult.Failure -> {
+ emit(state.toResult(HandleMethodError.UnknownError(result.error.customMessage).left()))
+ }
+ }
+ }
+
+ override fun invoke(): Flow> {
+ return delegate.invoke(initModel = WcMessageSignUseCase.SignModel(method.message))
+ }
+
+ private fun buildJsonResponse(data: com.tangem.blockchain.common.messagesigning.MessageSignatureResult): String {
+ val response = SignMessageResponse(
+ address = data.address,
+ signature = data.signature,
+ messageHash = data.messageHash,
+ )
+ return moshi.adapter(SignMessageResponse::class.java).toJson(response)
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(context: WcMethodUseCaseContext, method: WcBitcoinMethod.SignMessage): WcBitcoinSignMessageUseCase
+ }
+}
\ No newline at end of file
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/WcBitcoinSignPsbtUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/WcBitcoinSignPsbtUseCase.kt
new file mode 100644
index 0000000000..302376444a
--- /dev/null
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/WcBitcoinSignPsbtUseCase.kt
@@ -0,0 +1,142 @@
+package com.tangem.data.walletconnect.network.bitcoin
+
+import arrow.core.left
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import com.squareup.moshi.Moshi
+import com.tangem.blockchain.blockchains.bitcoin.walletconnect.models.SignInput
+import com.tangem.blockchain.common.TransactionData
+import com.tangem.blockchain.extensions.Result as SdkResult
+import com.tangem.core.analytics.api.AnalyticsEventHandler
+import com.tangem.data.walletconnect.respond.WcRespondService
+import com.tangem.data.walletconnect.sign.BaseWcSignUseCase
+import com.tangem.data.walletconnect.sign.SignCollector
+import com.tangem.data.walletconnect.sign.SignStateConverter.toResult
+import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
+import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate
+import com.tangem.datasource.di.SdkMoshi
+import com.tangem.domain.core.lce.LceFlow
+import com.tangem.domain.walletconnect.WcTransactionSignerProvider
+import com.tangem.domain.walletconnect.model.HandleMethodError
+import com.tangem.domain.walletconnect.model.WcBitcoinMethod
+import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck
+import com.tangem.domain.walletconnect.usecase.method.WcSignState
+import com.tangem.domain.walletconnect.usecase.method.WcTransactionUseCase
+import com.tangem.domain.walletmanager.WalletManagersFacade
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/**
+ * Use case for Bitcoin signPsbt WalletConnect method.
+ *
+ * Signs a Partially Signed Bitcoin Transaction (BIP-174 PSBT) with optional broadcast.
+ */
+@JsonClass(generateAdapter = true)
+internal data class SignPsbtResponse(
+ @Json(name = "psbt") val psbt: String,
+ @Json(name = "txid") val txid: String? = null,
+)
+
+@Suppress("LongParameterList")
+internal class WcBitcoinSignPsbtUseCase @AssistedInject constructor(
+ @Assisted override val context: WcMethodUseCaseContext,
+ @Assisted override val method: WcBitcoinMethod.SignPsbt,
+ private val walletManagersFacade: WalletManagersFacade,
+ private val signerProvider: WcTransactionSignerProvider,
+ override val respondService: WcRespondService,
+ override val analytics: AnalyticsEventHandler,
+ blockAidDelegate: BlockAidVerificationDelegate,
+ @SdkMoshi private val moshi: Moshi,
+) : BaseWcSignUseCase(),
+ WcTransactionUseCase {
+
+ override val wallet get() = context.session.wallet
+
+ override val securityStatus: LceFlow =
+ blockAidDelegate.getSecurityStatus(
+ network = network,
+ method = method,
+ rawSdkRequest = rawSdkRequest,
+ session = session,
+ accountAddress = context.accountAddress,
+ ).map { lce ->
+ lce.map { result -> BlockAidTransactionCheck.Result.Plain(result) }
+ }
+
+ override suspend fun SignCollector.onSign(state: WcSignState) {
+ // Update wallet manager to refresh UTXO data before processing Bitcoin transaction
+ walletManagersFacade.update(
+ userWalletId = wallet.walletId,
+ network = network,
+ extraTokens = emptySet(),
+ )
+
+ val walletManager = walletManagersFacade.getOrCreateWalletManager(wallet.walletId, network)
+ ?: run {
+ emit(state.toResult(HandleMethodError.UnknownError("Failed to create wallet manager").left()))
+ return
+ }
+
+ val signer = signerProvider.createSigner(wallet)
+ val signInputs = method.signInputs.map { input ->
+ SignInput(
+ address = input.address,
+ index = input.index,
+ sighashTypes = input.sighashTypes,
+ )
+ }
+ val signedPsbtResult = walletManager.signPsbt(
+ psbtBase64 = method.psbt,
+ signInputs = signInputs,
+ signer = signer,
+ )
+
+ when (signedPsbtResult) {
+ is SdkResult.Success -> {
+ val signedPsbt = signedPsbtResult.data
+ val txid = if (method.shouldBroadcast) {
+ when (val broadcastResult = walletManager.broadcastPsbt(signedPsbt)) {
+ is SdkResult.Success -> broadcastResult.data
+ is SdkResult.Failure -> {
+ val error = HandleMethodError.UnknownError(broadcastResult.error.customMessage).left()
+ emit(state.toResult(error))
+ return
+ }
+ }
+ } else {
+ null
+ }
+
+ val response = buildJsonResponse(signedPsbt, txid)
+ val wcRespondResult = respondService.respond(rawSdkRequest, response)
+ emit(state.toResult(wcRespondResult))
+ }
+ is SdkResult.Failure -> {
+ emit(state.toResult(HandleMethodError.UnknownError(signedPsbtResult.error.customMessage).left()))
+ }
+ }
+ }
+
+ override fun invoke(): Flow> {
+ val transactionData = TransactionData.Compiled(
+ value = TransactionData.Compiled.Data.RawString(method.psbt),
+ )
+ return delegate.invoke(transactionData)
+ }
+
+ private fun buildJsonResponse(signedPsbt: String, txid: String?): String {
+ val response = SignPsbtResponse(
+ psbt = signedPsbt,
+ txid = txid,
+ )
+ return moshi.adapter(SignPsbtResponse::class.java).toJson(response)
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(context: WcMethodUseCaseContext, method: WcBitcoinMethod.SignPsbt): WcBitcoinSignPsbtUseCase
+ }
+}
\ No newline at end of file
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/WcBitcoinTxAction.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/WcBitcoinTxAction.kt
new file mode 100644
index 0000000000..cb00b43745
--- /dev/null
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/bitcoin/WcBitcoinTxAction.kt
@@ -0,0 +1,8 @@
+package com.tangem.data.walletconnect.network.bitcoin
+
+import com.tangem.blockchain.common.transaction.Fee
+
+sealed interface WcBitcoinTxAction {
+
+ data class UpdateFee(val fee: Fee) : WcBitcoinTxAction
+}
\ No newline at end of file
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/SolanaBlockAidAddressConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/SolanaBlockAidAddressConverter.kt
index 6347e2f390..1a09c57487 100644
--- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/SolanaBlockAidAddressConverter.kt
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/SolanaBlockAidAddressConverter.kt
@@ -2,7 +2,7 @@ package com.tangem.data.walletconnect.network.solana
import com.tangem.blockchain.extensions.decodeBase58
import com.tangem.blockchain.extensions.encodeBase64NoWrap
-import com.tangem.data.walletconnect.utils.WC_TAG
+import com.tangem.domain.walletconnect.WC_TAG
import com.tangem.utils.converter.Converter
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt
index 02ce4d738f..c7e868cf94 100644
--- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt
@@ -8,7 +8,7 @@ import com.domain.blockaid.models.dapp.CheckDAppResult
import com.domain.blockaid.models.dapp.DAppData
import com.reown.walletkit.client.Wallet
import com.tangem.core.analytics.api.AnalyticsEventHandler
-import com.tangem.data.walletconnect.utils.WC_TAG
+import com.tangem.domain.walletconnect.WC_TAG
import com.tangem.data.walletconnect.utils.getDappOriginUrl
import com.tangem.domain.blockaid.BlockAidVerifier
import com.tangem.domain.walletconnect.WcAnalyticEvents
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt
index a747091d4a..d5aa2c0090 100644
--- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt
@@ -5,7 +5,7 @@ import arrow.core.left
import arrow.core.right
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
-import com.tangem.data.walletconnect.utils.WC_TAG
+import com.tangem.domain.walletconnect.WC_TAG
import com.tangem.data.walletconnect.utils.WcSdkObserver
import com.tangem.data.walletconnect.utils.getDappOriginUrl
import com.tangem.datasource.local.walletconnect.WalletConnectStore
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt
index 130530c326..80d677fed7 100644
--- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt
@@ -1,8 +1,9 @@
package com.tangem.data.walletconnect.request
import com.reown.walletkit.client.Wallet
+import com.tangem.data.walletconnect.BuildConfig
import com.tangem.data.walletconnect.respond.DefaultWcRespondService
-import com.tangem.data.walletconnect.utils.WC_TAG
+import com.tangem.domain.walletconnect.WC_TAG
import com.tangem.data.walletconnect.utils.WcSdkObserver
import com.tangem.data.walletconnect.utils.WcSdkSessionRequestConverter
import com.tangem.data.walletconnect.utils.getDappOriginUrl
@@ -43,6 +44,7 @@ internal class DefaultWcRequestService(
respondService.rejectRequestNonBlock(sr)
if (name.raw.startsWith("wallet_")) return
}
+
_wcRequest.trySend(name to sr)
}
@@ -62,6 +64,11 @@ internal class DefaultWcRequestService(
}
private fun saveRequest(request: WcSdkSessionRequest) {
+ // Skip caching in debug builds since filtering is disabled
+ if (BuildConfig.DEBUG) {
+ return
+ }
+
val hash = respondService.sessionRequestHash(request)
val now = DateTime.now().millis
respondService.cachedRequest.update { it + (now to hash) }
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestUseCaseFactory.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestUseCaseFactory.kt
index 41caf2afe1..7352709bd1 100644
--- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestUseCaseFactory.kt
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestUseCaseFactory.kt
@@ -4,7 +4,7 @@ import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.core.analytics.api.AnalyticsEventHandler
-import com.tangem.data.walletconnect.utils.WC_TAG
+import com.tangem.domain.walletconnect.WC_TAG
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.WcRequestUseCaseFactory
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt
index fdb3bf7bb1..ea4645374e 100644
--- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt
@@ -7,7 +7,7 @@ import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.toHexString
-import com.tangem.data.walletconnect.utils.WC_TAG
+import com.tangem.domain.walletconnect.WC_TAG
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.utils.logging.TangemLogger
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt
index c6fed93088..7d03acdda4 100644
--- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt
@@ -6,7 +6,7 @@ import arrow.core.right
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.core.analytics.api.AnalyticsEventHandler
-import com.tangem.data.walletconnect.utils.WC_TAG
+import com.tangem.domain.walletconnect.WC_TAG
import com.tangem.data.walletconnect.utils.WcNetworksConverter
import com.tangem.data.walletconnect.utils.WcSdkObserver
import com.tangem.data.walletconnect.utils.WcSdkSessionConverter
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BlockAidChainNameConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BlockAidChainNameConverter.kt
index 7f934668f6..410b86fca9 100644
--- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BlockAidChainNameConverter.kt
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BlockAidChainNameConverter.kt
@@ -29,6 +29,9 @@ internal object BlockAidChainNameConverter : Converter {
Blockchain.Solana -> "mainnet"
+ Blockchain.Bitcoin -> "bitcoin"
+ Blockchain.BitcoinTestnet -> "bitcoin-testnet"
+
else -> null
}
}
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt
index f0d71c240c..2f3ddc858d 100644
--- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt
@@ -6,6 +6,7 @@ import com.tangem.domain.blockaid.BlockAidVerifier
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.models.network.Network
+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.WcSession
@@ -26,10 +27,8 @@ internal class BlockAidVerificationDelegate @Inject constructor(
session: WcSession,
accountAddress: String?,
): LceFlow = flow {
- val failedResult = CheckTransactionResult(
- validation = ValidationResult.FAILED_TO_VALIDATE,
- simulation = SimulationResult.FailedToSimulate,
- )
+ val failedResult = createFailedResult()
+
if (accountAddress.isNullOrEmpty()) {
emit(Lce.Content(failedResult))
return@flow
@@ -43,18 +42,22 @@ internal class BlockAidVerificationDelegate @Inject constructor(
val methodName = when (method) {
is WcEthMethod -> rawSdkRequest.request.method
is WcSolanaMethod -> method.trimmedPrefixMethodName
+ is WcBitcoinMethod -> rawSdkRequest.request.method
is WcMethod.Unsupported -> {
emit(Lce.Content(failedResult))
return@flow
}
}
+
val params = when (method) {
is WcEthMethod -> TransactionParams.Evm(rawSdkRequest.request.params)
is WcSolanaMethod.SignAllTransaction -> TransactionParams.Solana(method.transaction)
is WcSolanaMethod.SignTransaction -> TransactionParams.Solana(listOf(method.transaction))
- is WcSolanaMethod.SignMessage -> {
- // BlockAid doesn't support solana_signMessage
- emit(Lce.Content(failedResult))
+ is WcSolanaMethod.SignMessage,
+ is WcBitcoinMethod,
+ -> {
+ // BlockAid doesn't support Solana message signing and Bitcoin methods
+ emit(Lce.Content(createSafeResult()))
return@flow
}
else -> {
@@ -81,4 +84,14 @@ internal class BlockAidVerificationDelegate @Inject constructor(
},
)
}
+
+ private fun createFailedResult() = CheckTransactionResult(
+ validation = ValidationResult.FAILED_TO_VALIDATE,
+ simulation = SimulationResult.FailedToSimulate,
+ )
+
+ private fun createSafeResult() = CheckTransactionResult(
+ validation = ValidationResult.SAFE,
+ simulation = SimulationResult.FailedToSimulate,
+ )
}
\ No newline at end of file
diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt
index 06637ecefd..4c70570c4d 100644
--- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt
+++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt
@@ -3,8 +3,6 @@ package com.tangem.data.walletconnect.utils
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
-const val WC_TAG = "Wallet Connect"
-
internal interface WcSdkObserver : WalletKit.WalletDelegate {
override val onSessionAuthenticate: ((Wallet.Model.SessionAuthenticate, Wallet.Model.VerifyContext) -> Unit)?
diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/TransactionData.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/TransactionData.kt
index 01fb3a9321..5895d2b738 100644
--- a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/TransactionData.kt
+++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/TransactionData.kt
@@ -33,4 +33,13 @@ sealed class TransactionParams {
data class Solana(
val transactions: List,
) : TransactionParams()
+
+ /**
+ * Parameters for Bitcoin transactions
+ *
+ * @property params JSON-encoded transaction parameters
+ */
+ data class Bitcoin(
+ val params: String,
+ ) : TransactionParams()
}
\ No newline at end of file
diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcBitcoinMethod.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcBitcoinMethod.kt
new file mode 100644
index 0000000000..f989194c01
--- /dev/null
+++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcBitcoinMethod.kt
@@ -0,0 +1,93 @@
+package com.tangem.domain.walletconnect.model
+
+/**
+ * Bitcoin WalletConnect method names.
+ *
+ * @see Bitcoin RPC Reference
+ */
+enum class WcBitcoinMethodName(override val raw: String) : WcMethodName {
+ SendTransfer("sendTransfer"),
+ GetAccountAddresses("getAccountAddresses"),
+ SignPsbt("signPsbt"),
+ SignMessage("signMessage"),
+}
+
+/**
+ * Bitcoin WalletConnect methods.
+ */
+sealed interface WcBitcoinMethod : WcMethod {
+ val methodName: String
+
+ /**
+ * Send a Bitcoin transfer transaction.
+ *
+ * @property account Source address (SegWit)
+ * @property recipientAddress Destination address
+ * @property amount Amount in satoshis
+ * @property memo Optional OP_RETURN memo
+ * @property changeAddress Optional custom change address
+ */
+ data class SendTransfer(
+ val account: String,
+ val recipientAddress: String,
+ val amount: String,
+ val memo: String?,
+ val changeAddress: String?,
+ ) : WcBitcoinMethod {
+ override val methodName: String = WcBitcoinMethodName.SendTransfer.raw
+ }
+
+ /**
+ * Get account addresses filtered by intention.
+ *
+ * @property account Connected account address
+ * @property intentions Optional filter ("payment", "ordinal")
+ */
+ data class GetAccountAddresses(
+ val account: String,
+ val intentions: List?,
+ ) : WcBitcoinMethod {
+ override val methodName: String = WcBitcoinMethodName.GetAccountAddresses.raw
+ }
+
+ /**
+ * Sign a PSBT (BIP-174).
+ *
+ * @property psbt PSBT in Base64 encoding
+ * @property signInputs List of inputs to sign
+ * @property shouldBroadcast Whether to broadcast after signing
+ */
+ data class SignPsbt(
+ val psbt: String,
+ val signInputs: List,
+ val shouldBroadcast: Boolean,
+ ) : WcBitcoinMethod {
+ override val methodName: String = WcBitcoinMethodName.SignPsbt.raw
+ }
+
+ /**
+ * Sign input specification for PSBT.
+ */
+ data class SignInput(
+ val address: String,
+ val index: Int,
+ val sighashTypes: List?,
+ )
+
+ /**
+ * Sign an arbitrary message using Bitcoin message signing format.
+ *
+ * @property account Connected account address
+ * @property message Message to sign
+ * @property address Optional specific address to sign with
+ * @property protocol Signing protocol ("ecdsa" or "bip322")
+ */
+ data class SignMessage(
+ val account: String,
+ val message: String,
+ val address: String?,
+ val protocol: String,
+ ) : WcBitcoinMethod {
+ override val methodName: String = WcBitcoinMethodName.SignMessage.raw
+ }
+}
\ No newline at end of file
diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcLogTag.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcLogTag.kt
new file mode 100644
index 0000000000..5fb6a63542
--- /dev/null
+++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcLogTag.kt
@@ -0,0 +1,6 @@
+package com.tangem.domain.walletconnect
+
+/**
+ * Common log tag for all WalletConnect-related logging across modules.
+ */
+const val WC_TAG = "WalletConnect"
\ No newline at end of file
diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcTransactionSignerProvider.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcTransactionSignerProvider.kt
new file mode 100644
index 0000000000..7cecd888b9
--- /dev/null
+++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcTransactionSignerProvider.kt
@@ -0,0 +1,21 @@
+package com.tangem.domain.walletconnect
+
+import com.tangem.blockchain.common.TransactionSigner
+import com.tangem.domain.models.wallet.UserWallet
+
+/**
+ * Provider for creating transaction signers for WalletConnect operations.
+ *
+ * This interface abstracts the creation of [TransactionSigner] instances
+ * to avoid direct dependency on card SDK configuration in wallet-connect module.
+ */
+interface WcTransactionSignerProvider {
+
+ /**
+ * Creates a transaction signer for the given wallet.
+ *
+ * @param wallet The user wallet to create a signer for
+ * @return A [TransactionSigner] instance for the wallet
+ */
+ fun createSigner(wallet: UserWallet): TransactionSigner
+}
\ No newline at end of file
diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/featuretoggle/WalletConnectFeatureToggles.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/featuretoggle/WalletConnectFeatureToggles.kt
new file mode 100644
index 0000000000..a48906360d
--- /dev/null
+++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/featuretoggle/WalletConnectFeatureToggles.kt
@@ -0,0 +1,5 @@
+package com.tangem.domain.walletconnect.featuretoggle
+
+interface WalletConnectFeatureToggles {
+ val isBitcoinEnabled: Boolean
+}
\ No newline at end of file
diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcGetAddressesUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcGetAddressesUseCase.kt
new file mode 100644
index 0000000000..5cdbd444fd
--- /dev/null
+++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcGetAddressesUseCase.kt
@@ -0,0 +1,41 @@
+package com.tangem.domain.walletconnect.usecase.method
+
+import arrow.core.Either
+import com.tangem.domain.walletconnect.model.HandleMethodError
+
+/**
+ * Base use case for WalletConnect methods that return wallet addresses.
+ *
+ * This is a non-signing operation that returns addresses immediately.
+ */
+interface WcGetAddressesUseCase : WcMethodUseCase, WcMethodContext {
+
+ /**
+ * Get wallet addresses.
+ *
+ * @return Either error or list of addresses with their metadata
+ */
+ suspend operator fun invoke(): Either
+
+ /**
+ * Reject the request.
+ */
+ fun reject()
+
+ /**
+ * Result containing wallet addresses.
+ */
+ data class GetAddressesResult(
+ val addresses: List,
+ )
+
+ /**
+ * Address information.
+ */
+ data class AddressInfo(
+ val address: String,
+ val publicKey: String?,
+ val path: String?,
+ val intention: String?,
+ )
+}
\ No newline at end of file
diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt
index 7a3bbca637..5b44bc3973 100644
--- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt
+++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt
@@ -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),
diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcInnerRoute.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcInnerRoute.kt
index bc9f386e88..637069b54e 100644
--- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcInnerRoute.kt
+++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcInnerRoute.kt
@@ -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
diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt
index 341ab307f2..ddfb9f8b3b 100644
--- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt
+++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt
@@ -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) {
diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt
index 620599339d..66f4e4fb8d 100644
--- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt
+++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt
@@ -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)
diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/addresses/WcGetAddressesComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/addresses/WcGetAddressesComponent.kt
new file mode 100644
index 0000000000..b57c6484cc
--- /dev/null
+++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/addresses/WcGetAddressesComponent.kt
@@ -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) {
+ }
+}
\ No newline at end of file
diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/TransactionParamsConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/TransactionParamsConverter.kt
index bc884a209f..d82c809cf4 100644
--- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/TransactionParamsConverter.kt
+++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/TransactionParamsConverter.kt
@@ -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 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
}
diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt
index e4969a134c..f1b56b37bc 100644
--- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt
+++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt
@@ -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
@@ -39,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,
diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/addresses/WcGetAddressesUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/addresses/WcGetAddressesUM.kt
new file mode 100644
index 0000000000..05b5667a53
--- /dev/null
+++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/addresses/WcGetAddressesUM.kt
@@ -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,
+ val isLoading: Boolean,
+ @DrawableRes val walletInteractionIcon: Int,
+ val onApprove: () -> Unit,
+ val onReject: () -> Unit,
+) {
+ data class AddressInfo(
+ val address: String,
+ val intention: String?,
+ )
+}
\ No newline at end of file
diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcGetAddressesModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcGetAddressesModel.kt
new file mode 100644
index 0000000000..11efd9327d
--- /dev/null
+++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcGetAddressesModel.kt
@@ -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()
+
+ init {
+ modelScope.launch {
+ val useCase = useCaseFactory.createUseCase(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))
+ }
+}
\ No newline at end of file
diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt
index 2f36707a60..16074b661c 100644
--- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt
+++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt
@@ -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(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
+ }
}
}