Updated on 2026-08-14
This commit is contained in:
parent
2b383418c0
commit
1326b5bab3
41 changed files with 1381 additions and 38 deletions
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -59,5 +59,9 @@
|
|||
{
|
||||
"name": "ADD_AND_MANAGE_TOKENS_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "WALLET_CONNECT_BITCOIN_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2150,6 +2150,8 @@
|
|||
<string name="wc_specify_networks_subtitle">At least one network is required for dApp connection</string>
|
||||
<string name="wc_specify_networks_title">Specify selected networks</string>
|
||||
<string name="wc_successfully_signed">Successfully signed</string>
|
||||
<string name="wc_get_addresses_title">Share Addresses</string>
|
||||
<string name="wc_get_addresses_addresses_title">Addresses to share</string>
|
||||
<string name="wc_transaction_flow_title" translatable="false">WalletConnect</string>
|
||||
<string name="wc_transaction_info_to_title">To</string>
|
||||
<string name="wc_transaction_request">Transaction request</string>
|
||||
|
|
|
|||
|
|
@ -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<SDKTransactionData.Uncompiled>,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ dependencies {
|
|||
/* Project - Core */
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.analytics)
|
||||
api(projects.core.configToggles)
|
||||
|
||||
/* DI */
|
||||
implementation(deps.hilt.core)
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ internal object BlockAidChainNameConverter : Converter<Network, String?> {
|
|||
|
||||
Blockchain.Solana -> "mainnet"
|
||||
|
||||
Blockchain.Bitcoin -> "bitcoin"
|
||||
Blockchain.BitcoinTestnet -> "bitcoin-testnet"
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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)?
|
||||
|
|
|
|||
|
|
@ -33,4 +33,13 @@ sealed class TransactionParams {
|
|||
data class Solana(
|
||||
val transactions: List<String>,
|
||||
) : TransactionParams()
|
||||
|
||||
/**
|
||||
* Parameters for Bitcoin transactions
|
||||
*
|
||||
* @property params JSON-encoded transaction parameters
|
||||
*/
|
||||
data class Bitcoin(
|
||||
val params: String,
|
||||
) : TransactionParams()
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.tangem.domain.walletconnect.model
|
||||
|
||||
/**
|
||||
* Bitcoin WalletConnect method names.
|
||||
*
|
||||
* @see <a href="https://docs.reown.com/advanced/multichain/rpc-reference/bitcoin-rpc">Bitcoin RPC Reference</a>
|
||||
*/
|
||||
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<String>?,
|
||||
) : 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<SignInput>,
|
||||
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<Int>?,
|
||||
)
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.domain.walletconnect
|
||||
|
||||
/**
|
||||
* Common log tag for all WalletConnect-related logging across modules.
|
||||
*/
|
||||
const val WC_TAG = "WalletConnect"
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.domain.walletconnect.featuretoggle
|
||||
|
||||
interface WalletConnectFeatureToggles {
|
||||
val isBitcoinEnabled: Boolean
|
||||
}
|
||||
|
|
@ -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<HandleMethodError, GetAddressesResult>
|
||||
|
||||
/**
|
||||
* Reject the request.
|
||||
*/
|
||||
fun reject()
|
||||
|
||||
/**
|
||||
* Result containing wallet addresses.
|
||||
*/
|
||||
data class GetAddressesResult(
|
||||
val addresses: List<AddressInfo>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Address information.
|
||||
*/
|
||||
data class AddressInfo(
|
||||
val address: String,
|
||||
val publicKey: String?,
|
||||
val path: String?,
|
||||
val intention: String?,
|
||||
)
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestBlockUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoItemUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
|
@ -58,7 +59,16 @@ internal class TransactionParamsConverter @Inject constructor() : Converter<Stri
|
|||
}
|
||||
}
|
||||
}
|
||||
loop(JSONArray(value))
|
||||
try {
|
||||
when (value.trimStart().firstOrNull()) {
|
||||
'[' -> loop(JSONArray(value))
|
||||
'{' -> loop(JSONObject(value))
|
||||
else -> loop(JSONArray(value)) // default to array for backward compatibility
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
TangemLogger.withTag("Wallet Connect").e("Failed to parse transaction params: ${e.message.orEmpty()}")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.features.walletconnect.transaction.entity.addresses
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM
|
||||
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM
|
||||
|
||||
/**
|
||||
* UI model for Bitcoin getAccountAddresses WalletConnect request.
|
||||
*/
|
||||
internal data class WcGetAddressesUM(
|
||||
val appInfo: WcTransactionAppInfoContentUM,
|
||||
val networkInfo: WcNetworkInfoUM,
|
||||
val addresses: List<AddressInfo>,
|
||||
val isLoading: Boolean,
|
||||
@DrawableRes val walletInteractionIcon: Int,
|
||||
val onApprove: () -> Unit,
|
||||
val onReject: () -> Unit,
|
||||
) {
|
||||
data class AddressInfo(
|
||||
val address: String,
|
||||
val intention: String?,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.features.walletconnect.transaction.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.domain.walletconnect.WcRequestUseCaseFactory
|
||||
import com.tangem.domain.walletconnect.model.HandleMethodError
|
||||
import com.tangem.domain.walletconnect.usecase.method.WcGetAddressesUseCase
|
||||
import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams
|
||||
import com.tangem.features.walletconnect.transaction.converter.WcHandleMethodErrorConverter
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Model for Bitcoin getAccountAddresses WalletConnect method.
|
||||
*/
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class WcGetAddressesModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val useCaseFactory: WcRequestUseCaseFactory,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<WcTransactionModelParams>()
|
||||
|
||||
init {
|
||||
modelScope.launch {
|
||||
val useCase = useCaseFactory.createUseCase<WcGetAddressesUseCase>(params.rawRequest)
|
||||
.onLeft { showErrorDialog(it) }
|
||||
.getOrNull() ?: return@launch
|
||||
|
||||
useCase.invoke()
|
||||
.onLeft { showErrorDialog(it) }
|
||||
.onRight { router.pop() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun showErrorDialog(error: HandleMethodError) {
|
||||
router.push(WcHandleMethodErrorConverter.convert(error))
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,8 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import com.tangem.domain.walletconnect.WC_TAG
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
|
|
@ -67,14 +69,36 @@ internal class WcSignTransactionModel @Inject constructor(
|
|||
|
||||
init {
|
||||
modelScope.launch {
|
||||
TangemLogger.withTag(WC_TAG).i("Creating use case...")
|
||||
useCase = useCaseFactory.createUseCase<WcMessageSignUseCase>(params.rawRequest)
|
||||
.onLeft { router.push(WcHandleMethodErrorConverter.convert(it)) }
|
||||
.getOrNull() ?: return@launch
|
||||
.onLeft { error ->
|
||||
TangemLogger.withTag(WC_TAG).e("Failed to create use case: $error")
|
||||
router.push(WcHandleMethodErrorConverter.convert(error))
|
||||
}
|
||||
.getOrNull() ?: run {
|
||||
TangemLogger.withTag(WC_TAG).e("Use case is null, exiting")
|
||||
return@launch
|
||||
}
|
||||
|
||||
TangemLogger.withTag(WC_TAG).i("Use case created successfully")
|
||||
TangemLogger.withTag(WC_TAG).i("Use case type: ${useCase.javaClass.simpleName}")
|
||||
TangemLogger.withTag(WC_TAG).i("Method: ${useCase.method}")
|
||||
|
||||
sendSignatureReceivedAnalytics(useCase)
|
||||
|
||||
TangemLogger.withTag(WC_TAG).i("Invoking use case...")
|
||||
useCase.invoke()
|
||||
.onEach { signState ->
|
||||
if (signingIsDone(signState)) return@onEach
|
||||
TangemLogger.withTag(WC_TAG).i("Sign state received: ${signState.javaClass.simpleName}")
|
||||
|
||||
if (signingIsDone(signState)) {
|
||||
TangemLogger.withTag(WC_TAG).i("Signing is DONE, not updating UI")
|
||||
return@onEach
|
||||
}
|
||||
|
||||
TangemLogger.withTag(WC_TAG).i("Converting to UI state...")
|
||||
val signTransactionUM = convertToUI(useCase, signState)
|
||||
TangemLogger.withTag(WC_TAG).i("UI state created, emitting...")
|
||||
_uiState.emit(signTransactionUM)
|
||||
}
|
||||
.launchIn(this)
|
||||
|
|
@ -101,7 +125,10 @@ internal class WcSignTransactionModel @Inject constructor(
|
|||
portfolioName = portfolioNameDelegate.createAccountTitleUM(useCase.session),
|
||||
),
|
||||
)
|
||||
is WcEthMethod.MessageSign, is WcSolanaMethod.SignMessage -> signTransactionUMConverter.convert(
|
||||
is WcEthMethod.MessageSign,
|
||||
is WcSolanaMethod.SignMessage,
|
||||
is com.tangem.domain.walletconnect.model.WcBitcoinMethod.SignMessage,
|
||||
-> signTransactionUMConverter.convert(
|
||||
WcSignTransactionUMConverter.Input(
|
||||
context = useCase,
|
||||
signState = signState,
|
||||
|
|
@ -110,7 +137,10 @@ internal class WcSignTransactionModel @Inject constructor(
|
|||
portfolioName = portfolioNameDelegate.createAccountTitleUM(useCase.session),
|
||||
),
|
||||
)
|
||||
else -> null
|
||||
else -> {
|
||||
TangemLogger.withTag(WC_TAG).e("UNSUPPORTED METHOD: ${useCase.method.javaClass.simpleName}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue