Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-17 15:32:56 +04:00
parent 2b383418c0
commit 1326b5bab3
41 changed files with 1381 additions and 38 deletions

View file

@ -34,6 +34,7 @@ dependencies {
/* Project - Core */
implementation(projects.core.utils)
implementation(projects.core.analytics)
api(projects.core.configToggles)
/* DI */
implementation(deps.hilt.core)

View file

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

View file

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

View file

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

View file

@ -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<String>? = 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<WcBitcoinSignInput>,
@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<Int>? = 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<WcBitcoinAddressInfo>,
)
/**
* 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,
)

View file

@ -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<HandleMethodError, WcGetAddressesUseCase.GetAddressesResult> {
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<AccountAddress>): String {
val addresses = accountAddresses.map { addr ->
AddressInfo(
address = addr.address,
publicKey = addr.publicKey,
path = addr.path,
intention = addr.intention,
)
}
return moshi.adapter<List<AddressInfo>>(
com.squareup.moshi.Types.newParameterizedType(List::class.java, AddressInfo::class.java),
).toJson(addresses)
}
@AssistedFactory
interface Factory {
fun create(
context: WcMethodUseCaseContext,
method: WcBitcoinMethod.GetAccountAddresses,
): WcBitcoinGetAccountAddressesUseCase
}
}

View file

@ -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 <a href="https://docs.reown.com/advanced/multichain/rpc-reference/bitcoin-rpc">Bitcoin RPC Reference</a>
*/
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<HandleMethodError, WcMethodUseCase> {
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<Throwable, WcBitcoinMethod?> {
val rawParams = request.request.params
return when (this) {
WcBitcoinMethodName.SendTransfer -> moshi.fromJson<WcBitcoinSendTransferRequest>(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<WcBitcoinGetAccountAddressesRequest>(rawParams)
.getOrElse { return it.left() }
?.let { req ->
WcBitcoinMethod.GetAccountAddresses(
account = req.account.orEmpty(),
intentions = req.intentions,
)
}
WcBitcoinMethodName.SignPsbt -> moshi.fromJson<WcBitcoinSignPsbtRequest>(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<WcBitcoinSignMessageRequest>(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"
}
}

View file

@ -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<WcBitcoinTxAction, TransactionData>(),
WcTransactionUseCase,
WcMutableFee {
override val wallet get() = context.session.wallet
private val transferAmount: Amount by lazy {
createAmountFromSatoshis(method.amount)
}
override val securityStatus: LceFlow<Throwable, BlockAidTransactionCheck.Result> =
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<TransactionData>.onSign(state: WcSignState<TransactionData>) {
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<TransactionData>.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<WcSignState<TransactionData>> = 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
}
}

View file

@ -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<Nothing, WcMessageSignUseCase.SignModel>(),
WcMessageSignUseCase {
override val wallet get() = context.session.wallet
// BlockAid doesn't support Bitcoin message signing
override val securityStatus: LceFlow<Throwable, CheckTransactionResult> = flowOf(
Lce.Content(
CheckTransactionResult(
validation = ValidationResult.FAILED_TO_VALIDATE,
simulation = SimulationResult.FailedToSimulate,
),
),
)
override suspend fun SignCollector<WcMessageSignUseCase.SignModel>.onSign(
state: WcSignState<WcMessageSignUseCase.SignModel>,
) {
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<WcSignState<WcMessageSignUseCase.SignModel>> {
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
}
}

View file

@ -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<Nothing, TransactionData>(),
WcTransactionUseCase {
override val wallet get() = context.session.wallet
override val securityStatus: LceFlow<Throwable, BlockAidTransactionCheck.Result> =
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<TransactionData>.onSign(state: WcSignState<TransactionData>) {
// 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<WcSignState<TransactionData>> {
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
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,6 +29,9 @@ internal object BlockAidChainNameConverter : Converter<Network, String?> {
Blockchain.Solana -> "mainnet"
Blockchain.Bitcoin -> "bitcoin"
Blockchain.BitcoinTestnet -> "bitcoin-testnet"
else -> null
}
}

View file

@ -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<Throwable, CheckTransactionResult> = 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,
)
}

View file

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