Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-28 12:02:46 +03:00
parent 2c739e2a4f
commit 70eda1b6e7
24 changed files with 445 additions and 140 deletions

View file

@ -121,6 +121,10 @@ class DialogManager : StoreSubscriber<GlobalState> {
onReject = state.dialog.onReject,
)
}
is WalletConnectDialog.SignTransactionDialog -> SignTransactionDialog.create(
preparedData = state.dialog.data,
context = context,
)
is BackupDialog.AttestationFailed -> AttestationFailedDialog.create(context)
is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context)
is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context)

View file

@ -5,7 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent
/**
[REDACTED_AUTHOR]
*/
sealed class WalletConnect(
internal sealed class WalletConnect(
event: String,
params: Map<String, String> = mapOf(),
error: Throwable? = null,
@ -65,7 +65,7 @@ sealed class WalletConnect(
FAIL("Fail"),
}
companion object {
private const val SUCCESS_CODE = "0"
private companion object {
const val SUCCESS_CODE = "0"
}
}

View file

@ -14,7 +14,8 @@ import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder
import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder
import timber.log.Timber
object BnbHelper {
internal object BnbHelper {
fun createMessageData(order: WCBinanceTransferOrder): BinanceMessageData.Transfer {
val input = order.msgs.first().inputs.first()
val output = order.msgs.first().inputs.first()

View file

@ -41,7 +41,7 @@ import java.util.concurrent.TimeUnit
import kotlin.collections.set
@Suppress("LargeClass")
class WalletConnectManager {
internal class WalletConnectManager {
private var cardId: String? = null
@ -348,6 +348,7 @@ class WalletConnectManager {
val hash = WalletConnectSdkHelper().signPersonalMessage(
hashToSign = data.hash,
networkId = activeData.wallet.blockchain?.toNetworkId() ?: "",
type = data.type,
derivationPath = activeData.wallet.derivationPath?.rawPath,
cardId = cardId,
)
@ -522,9 +523,9 @@ class WalletConnectManager {
}
}
typealias Topic = String
internal typealias Topic = String
data class WalletConnectActiveData(
internal data class WalletConnectActiveData(
val peerId: String,
val remotePeerId: String?,
val client: WCClient,
@ -547,7 +548,7 @@ data class WalletConnectActiveData(
}
@Suppress("MagicNumber")
class RetryInterceptor : Interceptor {
internal class RetryInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request: Request = chain.request()
val response = chain.proceed(request)

View file

@ -7,10 +7,7 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.blockchain.extensions.hexToBigDecimal
import com.tangem.blockchain.extensions.isAscii
import com.tangem.blockchain.extensions.*
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey
@ -25,8 +22,9 @@ import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTradeOrder
import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTransferOrder
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage
import com.tangem.tap.domain.walletconnect2.domain.TransactionType
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction
import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage
import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder
@ -45,6 +43,7 @@ import timber.log.Timber
import java.math.BigDecimal
import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam
@Suppress("LargeClass")
class WalletConnectSdkHelper {
@Suppress("MagicNumber")
@ -206,8 +205,7 @@ class WalletConnectSdkHelper {
walletPublicKey = data.walletManager.wallet.publicKey.seedKey,
derivationPath = data.walletManager.wallet.publicKey.derivationPath,
)
val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)
return when (result) {
return when (val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)) {
is CompletionResult.Success -> {
val hash = EthereumUtils.prepareTransactionToSend(
signature = result.data.signature,
@ -250,8 +248,7 @@ class WalletConnectSdkHelper {
walletPublicKey = wallet.publicKey.seedKey,
derivationPath = wallet.publicKey.derivationPath,
)
val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)
return when (result) {
return when (val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)) {
is CompletionResult.Success -> {
val key = wallet.publicKey.blockchainKey.toDecompressedPublicKey()
getBnbResultString(
@ -267,16 +264,17 @@ class WalletConnectSdkHelper {
}
fun prepareDataForPersonalSign(
message: WcEthereumSignMessage,
message: WcSignMessage,
topic: String,
metaName: String,
id: Long,
): WcPersonalSignData {
val messageData = when (message.type) {
WcEthereumSignMessage.WCSignType.MESSAGE,
WcEthereumSignMessage.WCSignType.PERSONAL_MESSAGE,
WcSignMessage.WCSignType.MESSAGE,
WcSignMessage.WCSignType.PERSONAL_MESSAGE,
-> createMessageData(message)
WcEthereumSignMessage.WCSignType.TYPED_MESSAGE -> EthereumUtils.makeTypedDataHash(message.data)
WcSignMessage.WCSignType.TYPED_MESSAGE -> EthereumUtils.makeTypedDataHash(message.data)
WcSignMessage.WCSignType.SOLANA_MESSAGE -> message.data.decodeBase58()
}
val messageString = message.data.hexToAscii()
@ -290,7 +288,7 @@ class WalletConnectSdkHelper {
id = id,
)
return WcPersonalSignData(
hash = messageData,
hash = requireNotNull(messageData) { "Message data must not be null" },
topic = topic,
id = id,
dialogData = dialogData,
@ -298,7 +296,7 @@ class WalletConnectSdkHelper {
)
}
private fun createMessageData(message: WcEthereumSignMessage): ByteArray {
private fun createMessageData(message: WcSignMessage): ByteArray {
val messageData = try {
message.data.removePrefix(HEX_PREFIX).hexToBytes()
} catch (exception: Exception) {
@ -332,6 +330,7 @@ class WalletConnectSdkHelper {
suspend fun signPersonalMessage(
hashToSign: ByteArray,
networkId: String,
type: WcSignMessage.WCSignType,
derivationPath: String?,
cardId: String?,
): String? {
@ -345,12 +344,16 @@ class WalletConnectSdkHelper {
)
return when (val result = tangemSdkManager.runTaskAsync(command, cardId)) {
is CompletionResult.Success -> {
val hash = result.data.signature
return EthereumUtils.prepareSignedMessageData(
signedHash = hash,
hashToSign = hashToSign,
publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(),
)
val signedHash = result.data.signature
return when (type) {
WcSignMessage.WCSignType.SOLANA_MESSAGE -> getSolanaResultString(signedHash)
else -> EthereumUtils.prepareSignedMessageData(
signedHash = signedHash,
hashToSign = hashToSign,
publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(),
)
}
}
is CompletionResult.Failure -> {
Timber.e(result.error.customMessage)
@ -359,12 +362,46 @@ class WalletConnectSdkHelper {
}
}
companion object {
private const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"
private const val HEX_PREFIX = "0x"
private const val DEFAULT_MAX_GASLIMIT = 350000
fun getBnbResultString(publicKey: String, signature: String): String {
return "{\"signature\":\"$signature\",\"publicKey\":\"$publicKey\"}"
suspend fun signTransaction(
hashToSign: ByteArray,
networkId: String,
type: TransactionType,
derivationPath: String?,
cardId: String?,
): String? {
val blockchain = Blockchain.fromNetworkId(networkId) ?: return null
val wallet = getWalletManager(blockchain, derivationPath)?.wallet ?: return null
val command = SignHashCommand(
hash = hashToSign,
walletPublicKey = wallet.publicKey.seedKey,
derivationPath = wallet.publicKey.derivationPath,
)
return when (val result = tangemSdkManager.runTaskAsync(command, cardId)) {
is CompletionResult.Success -> {
val signedHash = result.data.signature
when (type) {
TransactionType.SOLANA_TX -> {
getSolanaResultString(signedHash)
}
}
}
is CompletionResult.Failure -> {
Timber.e(result.error.customMessage)
null
}
}
}
private fun getSolanaResultString(signedHash: ByteArray) = "{ signature: \"${signedHash.encodeBase58()}\" }"
private fun getBnbResultString(publicKey: String, signature: String) =
"{\"signature\":\"$signature\",\"publicKey\":\"$publicKey\"}"
private companion object {
const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"
const val HEX_PREFIX = "0x"
const val DEFAULT_MAX_GASLIMIT = 350000
}
}

View file

@ -1,18 +1,18 @@
package com.tangem.tap.domain.walletconnect.extensions
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction
import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage
import com.trustwallet.walletconnect.models.WCPeerMeta
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction
fun WCPeerMeta.isDappSupported(): Boolean {
internal fun WCPeerMeta.isDappSupported(): Boolean {
return !unsupportedDappsList.any { this.url.contains(it) }
}
private val unsupportedDappsList: List<String> = listOf("dydx.exchange")
fun WCEthereumTransaction.toWcEthTransaction(): WcEthereumTransaction {
internal fun WCEthereumTransaction.toWcEthTransaction(): WcEthereumTransaction {
return WcEthereumTransaction(
from = from,
to = to,
@ -27,13 +27,13 @@ fun WCEthereumTransaction.toWcEthTransaction(): WcEthereumTransaction {
)
}
fun WCEthereumSignMessage.toWcEthereumSignMessage(): WcEthereumSignMessage {
return WcEthereumSignMessage(
internal fun WCEthereumSignMessage.toWcEthereumSignMessage(): WcSignMessage {
return WcSignMessage(
raw = raw,
type = when (type) {
WCEthereumSignMessage.WCSignType.MESSAGE -> WcEthereumSignMessage.WCSignType.MESSAGE
WCEthereumSignMessage.WCSignType.PERSONAL_MESSAGE -> WcEthereumSignMessage.WCSignType.PERSONAL_MESSAGE
WCEthereumSignMessage.WCSignType.TYPED_MESSAGE -> WcEthereumSignMessage.WCSignType.TYPED_MESSAGE
WCEthereumSignMessage.WCSignType.MESSAGE -> WcSignMessage.WCSignType.MESSAGE
WCEthereumSignMessage.WCSignType.PERSONAL_MESSAGE -> WcSignMessage.WCSignType.PERSONAL_MESSAGE
WCEthereumSignMessage.WCSignType.TYPED_MESSAGE -> WcSignMessage.WCSignType.TYPED_MESSAGE
},
)
}

View file

@ -4,8 +4,18 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.tap.domain.walletconnect2.domain.WcBlockchainHelper
import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles
internal class TangemWcBlockchainHelper(
private val featureToggles: WalletConnectFeatureToggles,
) : WcBlockchainHelper {
private val supportedNonEvmBlockchains = if (featureToggles.isSolanaTxSignEnabled) {
setOf(Blockchain.Solana, Blockchain.SolanaTestnet)
} else {
emptySet()
}
class TangemWcBlockchainHelper : WcBlockchainHelper {
override fun chainIdToNetworkIdOrNull(chainId: String): String? {
val parsedId = chainId.parseId() ?: return null
val blockchain = parsedId.chainIdToBlockchain()
@ -41,48 +51,57 @@ class TangemWcBlockchainHelper : WcBlockchainHelper {
val networkId = chainIdToNetworkIdOrNull(chainId) ?: return null
return Blockchain.fromNetworkId(networkId)?.fullName
}
}
private fun Blockchain.getCaip2ChainId(): String? {
if (this.isEvm()) return this.getChainId()?.toString()
private fun Blockchain.getCaip2ChainId(): String? {
if (this.isEvm()) return this.getChainId()?.toString()
return when (this) {
Blockchain.Solana -> "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ"
Blockchain.Polkadot -> "91b171bb158e2d3848fa23a9f1c25182"
Blockchain.Tron -> "0x2b6653dc"
else -> null
}
}
private fun Blockchain.getCaip2Namespace(): String? {
return when {
this.isEvm() -> EVM_NAMESPACE
supportedNonEvmBlockchains.contains(this) -> this.toNetworkId()
else -> null
}
}
private fun String.parseId(): Pair<String, String>? {
val parsed = this.split(CHAIN_SEPARATOR)
if (parsed.size != 2) return null
return parsed[0] to parsed[1]
}
private fun Pair<String, String>.chainIdToBlockchain(): Blockchain? {
return when (first) {
EVM_NAMESPACE -> {
second.toIntOrNull()
?.let(Blockchain::fromChainId)
}
else -> {
Blockchain.fromNetworkId(networkId = first)
.takeIf(supportedNonEvmBlockchains::contains)
return when (this) {
/*
* The WC sample application and documentation use a commented out chain ID. However, in real dApps
* uncommented is used.
* Docs: https://docs.walletconnect.com/advanced/multichain/chain-list
*
* Blockchain.Solana -> "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
* */
Blockchain.Solana -> "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ"
Blockchain.SolanaTestnet -> "z4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z"
Blockchain.Polkadot -> "91b171bb158e2d3848fa23a9f1c25182"
Blockchain.Tron -> "0x2b6653dc"
else -> null
}
}
}
private val supportedNonEvmBlockchains = emptySet<Blockchain>() // TODO: add supported networks
private fun Blockchain.getCaip2Namespace(): String? {
return when {
this.isEvm() -> EVM_NAMESPACE
supportedNonEvmBlockchains.contains(this) -> this.toNetworkId().substringBefore(TESTNET_SEPARATOR)
else -> null
}
}
private const val EVM_NAMESPACE = "eip155"
private const val CHAIN_SEPARATOR = ":"
private fun String.parseId(): Pair<String, String>? {
val parsed = this.split(CHAIN_SEPARATOR)
if (parsed.size != 2) return null
return parsed[0] to parsed[1]
}
private fun Pair<String, String>.chainIdToBlockchain(): Blockchain? {
return when (first) {
EVM_NAMESPACE -> {
second.toIntOrNull()
?.let(Blockchain::fromChainId)
}
else -> {
Blockchain.fromNetworkId(networkId = first)
.takeIf(supportedNonEvmBlockchains::contains)
}
}
}
private companion object {
const val EVM_NAMESPACE = "eip155"
const val CHAIN_SEPARATOR = ":"
const val TESTNET_SEPARATOR = "/"
}
}

View file

@ -12,7 +12,7 @@ import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen
import com.tangem.tap.store
import timber.log.Timber
class WalletConnectEventsHandlerImpl : WalletConnectEventsHandler {
internal class WalletConnectEventsHandlerImpl : WalletConnectEventsHandler {
override fun onProposalReceived(proposal: WalletConnectEvents.SessionProposal, networksFormatted: String) {
store.dispatchOnMain(
GlobalAction.ShowDialog(

View file

@ -18,9 +18,8 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import timber.log.Timber
import javax.inject.Inject
class WalletConnectRepositoryImpl @Inject constructor(
internal class DefaultWalletConnectRepository(
private val application: Application,
private val wcRequestDeserializer: WcJrpcRequestsDeserializer,
private val analyticsHandler: AnalyticsEventHandler,
@ -95,11 +94,11 @@ class WalletConnectRepositoryImpl @Inject constructor(
) {
// Triggered when wallet receives the session proposal sent by a Dapp
Timber.d("sessionProposal: $sessionProposal")
this@WalletConnectRepositoryImpl.sessionProposal = sessionProposal
this@DefaultWalletConnectRepository.sessionProposal = sessionProposal
val missingNetworks = findMissingNetworks(
namespaces = sessionProposal.requiredNamespaces,
userNamespaces = this@WalletConnectRepositoryImpl.userNamespaces ?: emptyMap(),
userNamespaces = this@DefaultWalletConnectRepository.userNamespaces ?: emptyMap(),
)
if (missingNetworks.isNotEmpty()) {
@ -116,7 +115,7 @@ class WalletConnectRepositoryImpl @Inject constructor(
val optionalWithoutMissingNetworks = removeMissingNetworks(
namespaces = sessionProposal.optionalNamespaces,
userNamespaces = this@WalletConnectRepositoryImpl.userNamespaces ?: emptyMap(),
userNamespaces = this@DefaultWalletConnectRepository.userNamespaces ?: emptyMap(),
)
scope.launch {
@ -334,6 +333,7 @@ class WalletConnectRepositoryImpl @Inject constructor(
),
)
}
Web3Wallet.respondSessionRequest(
params = Wallet.Params.SessionRequestResponse(
sessionTopic = requestData.topic,
@ -342,15 +342,19 @@ class WalletConnectRepositoryImpl @Inject constructor(
result = result,
),
),
onSuccess = {},
onError = {
onSuccess = { response ->
Timber.d("Session request responded successfully: $response")
},
onError = { error ->
Timber.d(error.throwable, "Error while responging session request")
WalletConnect.RequestHandledParams(
dAppName = session?.name ?: "",
dAppUrl = session?.url ?: "",
methodName = requestData.method,
blockchain = requestData.blockchain,
errorCode = WalletConnectError.ValidationError.error,
errorDescription = it.throwable.message,
errorDescription = error.throwable.message,
)
},
)

View file

@ -3,15 +3,13 @@ package com.tangem.tap.domain.walletconnect2.data
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.files.FileReader
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.domain.walletconnect2.domain.models.Session
import timber.log.Timber
import javax.inject.Inject
class WalletConnectSessionsRepositoryImpl @Inject constructor(
@SdkMoshi private val moshi: Moshi,
internal class DefaultWalletConnectSessionsRepository(
private val moshi: Moshi,
private val fileReader: FileReader,
) :
WalletConnectSessionsRepository {

View file

@ -1,15 +1,22 @@
package com.tangem.tap.domain.walletconnect2.di
import android.app.Application
import com.squareup.moshi.Moshi
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.files.FileReader
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
import com.tangem.tap.domain.walletconnect2.app.WalletConnectEventsHandlerImpl
import com.tangem.tap.domain.walletconnect2.data.WalletConnectRepositoryImpl
import com.tangem.tap.domain.walletconnect2.data.WalletConnectSessionsRepositoryImpl
import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectSessionsRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer
import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -20,19 +27,21 @@ import javax.inject.Singleton
@Module
@InstallIn(ActivityComponent::class)
class WalletConnectInteractorModule {
internal object WalletConnectInteractorModule {
@Provides
@ActivityScoped
fun provideWalletConnectInteractor(
wcRepository: WalletConnectRepository,
wcSessionsRepository: WalletConnectSessionsRepository,
walletConnectFeatureToggles: WalletConnectFeatureToggles,
): WalletConnectInteractor {
return WalletConnectInteractor(
handler = WalletConnectEventsHandlerImpl(),
walletConnectRepository = wcRepository,
sessionsRepository = wcSessionsRepository,
sdkHelper = WalletConnectSdkHelper(),
blockchainHelper = TangemWcBlockchainHelper(),
blockchainHelper = TangemWcBlockchainHelper(walletConnectFeatureToggles),
dispatcher = AppCoroutineDispatcherProvider(),
)
}
@ -40,15 +49,37 @@ class WalletConnectInteractorModule {
@Module
@InstallIn(SingletonComponent::class)
interface WalletConnectModule {
internal object WalletConnectModule {
@Binds
@Provides
@Singleton
fun bindWalletConnectRepository(repository: WalletConnectRepositoryImpl): WalletConnectRepository
fun provideWalletConnectFeatureToggles(featureTogglesManager: FeatureTogglesManager): WalletConnectFeatureToggles {
return WalletConnectFeatureToggles(featureTogglesManager)
}
@Binds
@Provides
@Singleton
fun bindWalletConnectSessionsRepository(
repository: WalletConnectSessionsRepositoryImpl,
): WalletConnectSessionsRepository
fun provideWalletConnectRepository(
application: Application,
wcRequestDeserializer: WcJrpcRequestsDeserializer,
analyticsHandler: AnalyticsEventHandler,
): WalletConnectRepository {
return DefaultWalletConnectRepository(
application = application,
wcRequestDeserializer = wcRequestDeserializer,
analyticsHandler = analyticsHandler,
)
}
@Provides
@Singleton
fun provideWalletConnectSessionsRepository(
@SdkMoshi moshi: Moshi,
fileReader: FileReader,
): WalletConnectSessionsRepository {
return DefaultWalletConnectSessionsRepository(
moshi = moshi,
fileReader = fileReader,
)
}
}

View file

@ -209,7 +209,7 @@ class WalletConnectInteractor(
val currentRequest = this.currentRequest
if (currentRequest == null || request.topic != currentRequest.topic) return
val networkId = blockchainHelper.chainIdToNetworkIdOrNull(currentRequest.chainId ?: "") ?: return
val networkId = blockchainHelper.chainIdToNetworkIdOrNull(currentRequest.chainId.orEmpty()) ?: return
val signedHash = when (request) {
is WcPreparedRequest.BnbTransaction -> sdkHelper.signBnbTransaction(
data = request.preparedRequestData.data.data,
@ -224,13 +224,19 @@ class WalletConnectInteractor(
is WcPreparedRequest.EthSign -> sdkHelper.signPersonalMessage(
hashToSign = request.preparedRequestData.hash,
networkId = networkId,
type = request.preparedRequestData.type,
derivationPath = request.derivationPath,
cardId = cardId,
)
is WcPreparedRequest.SignTransaction -> sdkHelper.signTransaction(
hashToSign = request.preparedRequestData.hashToSign,
networkId = networkId,
type = request.preparedRequestData.type,
derivationPath = request.derivationPath,
cardId = cardId,
)
}
Timber.d("Signed hash: $signedHash")
val requestData = RequestData(
topic = request.topic,
requestId = request.requestId,

View file

@ -2,15 +2,17 @@ package com.tangem.tap.domain.walletconnect2.domain
import com.squareup.moshi.*
import com.tangem.datasource.di.SdkMoshi
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WCBinanceTxConfirmParam
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceCancelOrder
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTxConfirmParam
import com.tangem.tap.domain.walletconnect2.domain.models.solana.SolanaSignMessage
import com.tangem.tap.domain.walletconnect2.domain.models.solana.SolanaTransactionRequest
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
enum class WcJrpcMethods(val code: String) {
internal enum class WcJrpcMethods(val code: String) {
ETH_SIGN("eth_sign"),
ETH_PERSONAL_SIGN("personal_sign"),
@ -18,10 +20,14 @@ enum class WcJrpcMethods(val code: String) {
ETH_SIGN_TYPE_DATA_V4("eth_signTypedData_v4"),
ETH_SIGN_TRANSACTION("eth_signTransaction"),
ETH_SEND_TRANSACTION("eth_sendTransaction"),
BNB_SIGN("bnb_sign"),
BNB_TRANSACTION_CONFIRM("bnb_tx_confirmation"),
SIGN_TRANSACTION("trust_signTransaction"),
WALLET_ADD_ETHEREUM_CHAIN("wallet_addEthereumChain"),
SOLANA_SIGN_TX("solana_signTransaction"),
SOLANA_SIGN_MESSAGE("solana_signMessage"),
;
companion object {
@ -30,7 +36,7 @@ enum class WcJrpcMethods(val code: String) {
}
@JsonClass(generateAdapter = true)
data class WCSignTransaction(
data class WcSignTransaction(
@Json(name = "network")
val network: Int,
@ -39,13 +45,7 @@ data class WCSignTransaction(
) : WcRequestData
@JsonClass(generateAdapter = true)
data class WcAddChain(
@Json(name = "chainId")
val chainId: String,
) : WcRequestData
@JsonClass(generateAdapter = true)
data class WcEthereumSignMessage(
data class WcSignMessage(
@Json(name = "raw")
val raw: List<String>,
@ -53,7 +53,7 @@ data class WcEthereumSignMessage(
val type: WCSignType,
) : WcRequestData {
enum class WCSignType {
MESSAGE, PERSONAL_MESSAGE, TYPED_MESSAGE
MESSAGE, PERSONAL_MESSAGE, TYPED_MESSAGE, SOLANA_MESSAGE,
}
/**
@ -63,6 +63,7 @@ data class WcEthereumSignMessage(
* - MESSAGE: `[address, data ]`
* - TYPED_MESSAGE: `[address, data]`
* - PERSONAL_MESSAGE: `[data, address]`
* - SOLANA_MESSAGE: `[publicKey (address), message]`
*
* reference: https://docs.walletconnect.org/json-rpc/ethereum#eth_signtypeddata
*/
@ -79,6 +80,21 @@ data class WcEthereumSignMessage(
}
}
@JsonClass(generateAdapter = true)
data class WcSignMessageData(
@Json(name = "address")
val address: String,
@Json(name = "message")
val message: String,
) : WcRequestData
@JsonClass(generateAdapter = true)
data class WcAddChain(
@Json(name = "chainId")
val chainId: String,
) : WcRequestData
@JsonClass(generateAdapter = true)
data class WcEthereumTransaction(
@Json(name = "from")
@ -117,20 +133,21 @@ interface WcRequestData
data class WcCustomRequestData(val data: String) : WcRequestData
sealed class WcRequest(open val data: WcRequestData) {
data class EthSign(override val data: WcEthereumSignMessage) : WcRequest(data)
data class EthSign(override val data: WcSignMessage) : WcRequest(data)
data class EthSignTransaction(override val data: WcEthereumTransaction) : WcRequest(data)
data class EthSendTransaction(override val data: WcEthereumTransaction) : WcRequest(data)
data class BnbTrade(override val data: WcBinanceTradeOrder) : WcRequest(data)
data class BnbCancel(override val data: WcBinanceCancelOrder) : WcRequest(data)
data class BnbTransfer(override val data: WcBinanceTransferOrder) : WcRequest(data)
data class BnbTxConfirm(override val data: WCBinanceTxConfirmParam) : WcRequest(data)
data class SignTransaction(override val data: WCSignTransaction) : WcRequest(data)
data class BnbTxConfirm(override val data: WcBinanceTxConfirmParam) : WcRequest(data)
data class SignTransaction(override val data: WcSignTransaction) : WcRequest(data)
data class AddChain(override val data: WcAddChain) : WcRequest(data)
data class CustomRequest(override val data: WcCustomRequestData) : WcRequest(data)
data class SolanaSignRequest(override val data: SolanaTransactionRequest) : WcRequest(data)
}
@Singleton
class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi: Moshi) {
internal class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi: Moshi) {
@Suppress("ComplexMethod", "LongMethod")
fun deserialize(method: String, params: String): WcRequest {
@ -154,9 +171,9 @@ class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi
val deserializedParams = moshi.adapter<List<String>>(
Types.newParameterizedType(List::class.java, String::class.java),
).fromJsonOrNull(params) ?: return customRequest
val data = WcEthereumSignMessage(
val data = WcSignMessage(
raw = deserializedParams,
type = WcEthereumSignMessage.WCSignType.MESSAGE,
type = WcSignMessage.WCSignType.MESSAGE,
)
WcRequest.EthSign(data = data)
}
@ -164,9 +181,9 @@ class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi
val deserializedParams = moshi.adapter<List<String>>(
Types.newParameterizedType(List::class.java, String::class.java),
).fromJsonOrNull(params) ?: return customRequest
val data = WcEthereumSignMessage(
val data = WcSignMessage(
raw = deserializedParams,
type = WcEthereumSignMessage.WCSignType.PERSONAL_MESSAGE,
type = WcSignMessage.WCSignType.PERSONAL_MESSAGE,
)
WcRequest.EthSign(data = data)
}
@ -175,9 +192,9 @@ class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi
params.substring(params.indexOf("\"") + 1, params.indexOf("\"", startIndex = 2)),
params.substring(params.indexOfFirst { it == '{' }, params.indexOfLast { it == '}' } + 1),
)
val data = WcEthereumSignMessage(
val data = WcSignMessage(
deserializedParams,
WcEthereumSignMessage.WCSignType.TYPED_MESSAGE,
WcSignMessage.WCSignType.TYPED_MESSAGE,
)
Timber.d("TypedData params: $deserializedParams")
WcRequest.EthSign(data)
@ -186,23 +203,43 @@ class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi
return deserializeBnb(moshi, params) ?: customRequest
}
WcJrpcMethods.BNB_TRANSACTION_CONFIRM -> {
val deserializedParams = moshi.adapter<List<WCBinanceTxConfirmParam>>(
Types.newParameterizedType(List::class.java, WCBinanceTxConfirmParam::class.java),
val deserializedParams = moshi.adapter<List<WcBinanceTxConfirmParam>>(
Types.newParameterizedType(List::class.java, WcBinanceTxConfirmParam::class.java),
).fromJsonFirstOrNull(params) ?: return customRequest
WcRequest.BnbTxConfirm(data = deserializedParams)
}
WcJrpcMethods.SIGN_TRANSACTION -> {
val deserializedParams = moshi.adapter<List<WCSignTransaction>>(
Types.newParameterizedType(List::class.java, WCSignTransaction::class.java),
val deserializedParams = moshi.adapter<List<WcSignTransaction>>(
Types.newParameterizedType(List::class.java, WcSignTransaction::class.java),
).fromJsonFirstOrNull(params) ?: return customRequest
WcRequest.SignTransaction(data = deserializedParams)
}
WcJrpcMethods.WALLET_ADD_ETHEREUM_CHAIN -> {
val deserializedParams: WcAddChain = moshi.adapter<List<WcAddChain>>(
Types.newParameterizedType(List::class.java, WcAddChain::class.java),
).fromJsonFirstOrNull(params) ?: return customRequest
WcRequest.AddChain(data = deserializedParams)
}
WcJrpcMethods.SOLANA_SIGN_TX -> {
val tx = moshi.adapter<SolanaTransactionRequest>(SolanaTransactionRequest::class.java)
.fromJsonOrNull(params)
?: return customRequest
WcRequest.SolanaSignRequest(data = tx)
}
WcJrpcMethods.SOLANA_SIGN_MESSAGE -> {
val signMessage = moshi.adapter<SolanaSignMessage>(SolanaSignMessage::class.java)
.fromJsonOrNull(params)
?: return customRequest
val data = WcSignMessage(
raw = listOf(signMessage.publicKey, signMessage.message),
type = WcSignMessage.WCSignType.SOLANA_MESSAGE,
)
WcRequest.EthSign(data = data)
}
}
}

View file

@ -30,4 +30,21 @@ sealed class WcPreparedRequest(
requestId: Long,
derivationPath: String?,
) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath)
class SignTransaction(
override val preparedRequestData: WcGenericTransactionData,
topic: String,
requestId: Long,
derivationPath: String?,
) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath)
}
data class WcGenericTransactionData(
val hashToSign: ByteArray,
val dAppName: String,
val type: TransactionType,
)
enum class TransactionType {
SOLANA_TX,
}

View file

@ -1,12 +1,13 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
import com.tangem.tap.domain.walletconnect2.domain.mapper.mapToTransaction
import com.tangem.tap.domain.walletconnect2.domain.models.BnbData
import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents
import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType
class WcSessionRequestConverter(
internal class WcSessionRequestConverter(
private val blockchainHelper: WcBlockchainHelper,
private val sessionsRepository: WalletConnectSessionsRepository,
private val sdkHelper: WalletConnectSdkHelper,
@ -107,6 +108,20 @@ class WcSessionRequestConverter(
derivationPath = derivationPath,
)
}
is WcRequest.SolanaSignRequest -> {
val data = request.data.mapToTransaction()
WcPreparedRequest.SignTransaction(
preparedRequestData = WcGenericTransactionData(
hashToSign = data.getSerializedMessage(),
dAppName = sessionRequest.metaName,
type = TransactionType.SOLANA_TX,
),
topic = sessionRequest.topic,
requestId = sessionRequest.id,
derivationPath = derivationPath,
)
}
else -> null
}
}
@ -118,6 +133,7 @@ class WcSessionRequestConverter(
is WcRequest.EthSendTransaction -> request.data.from
is WcRequest.EthSignTransaction -> request.data.from
is WcRequest.EthSign -> request.data.address
is WcRequest.SolanaSignRequest -> request.data.feePayer
else -> null
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.tap.domain.walletconnect2.domain.mapper
import com.tangem.blockchain.blockchains.solana.solanaj.core.SolanaTransaction
import com.tangem.tap.domain.walletconnect2.domain.models.solana.SolanaTransactionRequest
import org.p2p.solanaj.core.AccountMeta
import org.p2p.solanaj.core.PublicKey
import org.p2p.solanaj.core.TransactionInstruction
internal fun SolanaTransactionRequest.mapToTransaction(): SolanaTransaction {
val from = PublicKey(feePayer)
val instructions = instructions.map(SolanaTransactionRequest.Instruction::mapToInstruction)
return SolanaTransaction(from)
.apply {
instructions.forEach(::addInstruction)
setRecentBlockHash(recentBlockhash)
}
}
private fun SolanaTransactionRequest.Instruction.mapToInstruction() = TransactionInstruction(
/* programId = */ PublicKey(programId),
/* keys = */ keys.map(SolanaTransactionRequest.Key::mapToAccountMeta),
/* data = */ data.toByteArray(),
)
private fun SolanaTransactionRequest.Key.mapToAccountMeta() = AccountMeta(
/* publicKey = */ PublicKey(publicKey),
/* isSigner = */ isSigner,
/* isWritable = */ isWritable,
)

View file

@ -13,7 +13,7 @@ open class WcBinanceOrder<T>(
val msgs: List<T>,
) : WcRequestData
data class WCBinanceTxConfirmParam(
data class WcBinanceTxConfirmParam(
val ok: Boolean,
val errorMsg: String?,
) : WcRequestData

View file

@ -0,0 +1,13 @@
package com.tangem.tap.domain.walletconnect2.domain.models.solana
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class SolanaSignMessage(
@Json(name = "pubkey")
val publicKey: String,
@Json(name = "message")
val message: String,
)

View file

@ -0,0 +1,42 @@
package com.tangem.tap.domain.walletconnect2.domain.models.solana
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
@JsonClass(generateAdapter = true)
data class SolanaTransactionRequest(
@Json(name = "feePayer")
val feePayer: String,
@Json(name = "recentBlockhash")
val recentBlockhash: String,
@Json(name = "instructions")
val instructions: List<Instruction>,
) : WcRequestData {
@JsonClass(generateAdapter = true)
data class Instruction(
@Json(name = "programId")
val programId: String,
@Json(name = "data")
val data: List<Byte>,
@Json(name = "keys")
val keys: List<Key>,
)
@JsonClass(generateAdapter = true)
data class Key(
@Json(name = "isSigner")
val isSigner: Boolean,
@Json(name = "isWritable")
val isWritable: Boolean,
@Json(name = "pubkey")
val publicKey: String,
)
}

View file

@ -0,0 +1,11 @@
package com.tangem.tap.domain.walletconnect2.toggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
internal class WalletConnectFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) {
val isSolanaTxSignEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "WC_SOLANA_TX_SIGN_ENABLED")
}

View file

@ -375,6 +375,7 @@ class WalletConnectMiddleware {
is WcPreparedRequest.BnbTransaction -> WalletConnectDialog.BnbTransactionDialog(request)
is WcPreparedRequest.EthTransaction -> WalletConnectDialog.RequestTransaction(request)
is WcPreparedRequest.EthSign -> WalletConnectDialog.PersonalSign(request)
is WcPreparedRequest.SignTransaction -> WalletConnectDialog.SignTransactionDialog(request)
}
store.dispatch(GlobalAction.ShowDialog(dialog))
}

View file

@ -8,8 +8,8 @@ import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.core.navigation.StateDialog
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents
import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
@ -117,6 +117,10 @@ sealed class WalletConnectDialog : StateDialog {
data class BnbTransactionDialog(
val data: WcPreparedRequest.BnbTransaction,
) : WalletConnectDialog()
data class SignTransactionDialog(
val data: WcPreparedRequest.SignTransaction,
) : WalletConnectDialog()
}
data class WcTransactionData(
@ -138,7 +142,7 @@ data class WcPersonalSignData(
val topic: String,
val id: Long,
val dialogData: PersonalSignDialogData,
val type: WcEthereumSignMessage.WCSignType,
val type: WcSignMessage.WCSignType,
)
sealed class BinanceMessageData(

View file

@ -0,0 +1,29 @@
package com.tangem.tap.features.details.ui.walletconnect.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.store
import com.tangem.wallet.R
internal object SignTransactionDialog {
fun create(preparedData: WcPreparedRequest.SignTransaction, context: Context): AlertDialog {
val message = context.getString(R.string.wallet_connect_alert_sign_message, "")
return AlertDialog.Builder(context).apply {
setTitle(context.getString(R.string.wallet_connect_title))
setMessage(message)
setPositiveButton(context.getText(R.string.common_sign)) { _, _ ->
store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData))
}
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
store.dispatch(WalletConnectAction.RejectRequest(preparedData.topic, preparedData.requestId))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}