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 + } } }