Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-11 01:42:48 +07:00
parent 5762ba0d16
commit 17c7ae519c
14 changed files with 154 additions and 54 deletions

View file

@ -4,7 +4,6 @@ import com.tangem.Message
import com.tangem.blockchain.blockchains.ethereum.EthereumGasLoader
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
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.*
@ -16,6 +15,7 @@ import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.Basic.TransactionSent.MemoType
import com.tangem.data.walletconnect.network.ethereum.LegacySdkHelper
import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.safeUpdate
@ -357,36 +357,9 @@ class WalletConnectSdkHelper {
)
}
private fun createMessageData(message: WcSignMessage): ByteArray {
val messageData = try {
message.data.removePrefix(HEX_PREFIX).hexToBytes()
} catch (exception: Exception) {
message.data.asciiToHex()?.hexToBytes() ?: byteArrayOf()
}
private fun createMessageData(message: WcSignMessage): ByteArray = LegacySdkHelper.createMessageData(message.data)
val prefixData = (ETH_MESSAGE_PREFIX + messageData.size.toString()).toByteArray()
return (prefixData + messageData).toKeccak()
}
private fun String.hexToAscii(): String? {
return try {
removePrefix(HEX_PREFIX).hexToBytes()
.map {
val char = it.toInt().toChar()
if (char.isAscii()) char else return null
}
.joinToString("")
} catch (exception: Exception) {
return null
}
}
private fun String.asciiToHex(): String? {
return map {
if (!it.isAscii()) return null
Integer.toHexString(it.code)
}.joinToString("")
}
private fun String.hexToAscii(): String? = LegacySdkHelper.hexToAscii(hex = this)
suspend fun signPersonalMessage(
hashToSign: ByteArray,
@ -535,7 +508,6 @@ class WalletConnectSdkHelper {
"{\"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
// TODO remove after [REDACTED_JIRA]

View file

@ -16,6 +16,7 @@ dependencies {
implementation(projects.domain.walletConnect.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.card)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.models)

View file

@ -30,7 +30,6 @@ import com.tangem.domain.walletconnect.request.WcRequestService
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -96,7 +95,6 @@ internal object WalletConnectDataModule {
store: WalletConnectStore,
dispatchers: CoroutineDispatcherProvider,
legacyStore: WalletConnectSessionsRepository,
getUserWallet: GetUserWalletUseCase,
getWallets: GetWalletsUseCase,
): DefaultWcSessionsManager {
val scope = CoroutineScope(SupervisorJob() + dispatchers.io)
@ -105,7 +103,6 @@ internal object WalletConnectDataModule {
dispatchers = dispatchers,
legacyStore = legacyStore,
getWallets = getWallets,
getUserWallet = getUserWallet,
scope = scope,
)
}

View file

@ -0,0 +1,110 @@
package com.tangem.data.walletconnect.network.ethereum
import arrow.core.left
import arrow.core.right
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak
import com.tangem.blockchain.common.HEX_PREFIX
import com.tangem.blockchain.extensions.isAscii
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.data.walletconnect.sign.BaseWcSignUseCase
import com.tangem.data.walletconnect.sign.OnSign
import com.tangem.data.walletconnect.sign.SignStateConverter.toResult
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.domain.card.models.TwinKey
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.usecase.ethereum.WcPersonalEthSignUseCase
import com.tangem.domain.walletconnect.usecase.sign.WcSignState
import com.tangem.domain.walletmanager.WalletManagersFacade
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
internal class DefaultWcPersonalEthSignUseCase(
override val respondService: WcRespondService,
override val context: WcMethodUseCaseContext,
private val method: WcEthMethod.PersonalEthSign,
private val walletManagersFacade: WalletManagersFacade,
private val cardRepository: CardSdkConfigRepository,
) : BaseWcSignUseCase<Nothing, WcPersonalEthSignUseCase.SignModel>(),
WcPersonalEthSignUseCase {
override val onSign: OnSign<WcPersonalEthSignUseCase.SignModel> = collector@{ state ->
val hashToSign = LegacySdkHelper.createMessageData(state.signModel.rawMsg)
val userWallet = session.wallet
val card = userWallet.scanResponse.card
val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWallet.walletId, network)
?: return@collector
val signer = cardRepository.getCommonSigner(
cardId = card.cardId.takeIf { isCardNotBackedUp },
twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse),
)
val signResult = when (val signResult = signer.sign(hashToSign, walletManager.wallet.publicKey)) {
is CompletionResult.Failure -> signResult.error.left()
is CompletionResult.Success -> signResult.data.right()
}
val signedHash = signResult
.onLeft { emit(state.toResult(it.left())) }
.getOrNull() ?: return@collector
val respond = EthereumUtils.prepareSignedMessageData(
signedHash = signedHash,
hashToSign = hashToSign,
publicKey = walletManager.wallet.publicKey.blockchainKey.toDecompressedPublicKey(),
)
val wcRespondResult = respondService.respond(rawSdkRequest, respond)
emit(state.toResult(wcRespondResult))
}
override fun invoke(): Flow<WcSignState<WcPersonalEthSignUseCase.SignModel>> = flow {
val model = WcPersonalEthSignUseCase.SignModel(
rawMsg = method.message,
account = method.account,
humanMsg = LegacySdkHelper.hexToAscii(method.message).orEmpty(),
)
emitAll(delegate(model))
}
}
object LegacySdkHelper {
private const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"
fun createMessageData(message: String): ByteArray {
val messageData = try {
message.removePrefix(HEX_PREFIX).hexToBytes()
} catch (exception: Exception) {
message.asciiToHex()?.hexToBytes() ?: byteArrayOf()
}
val prefixData = (ETH_MESSAGE_PREFIX + messageData.size.toString()).toByteArray()
return (prefixData + messageData).toKeccak()
}
fun hexToAscii(hex: String): String? {
return try {
hex.removePrefix(HEX_PREFIX).hexToBytes().map {
val char = it.toInt().toChar()
if (char.isAscii()) char else return null
}.joinToString("")
} catch (exception: Exception) {
return null
}
}
private fun String.asciiToHex(): String? {
return map {
if (!it.isAscii()) return null
Integer.toHexString(it.code)
}.joinToString("")
}
}

View file

@ -10,8 +10,7 @@ import com.tangem.domain.walletconnect.model.WcMethod
import com.tangem.domain.walletconnect.model.WcRequest
import com.tangem.domain.walletconnect.usecase.WcUseCase
import com.tangem.domain.walletconnect.usecase.WcUseCasesFlowProvider
import com.tangem.domain.walletconnect.usecase.ethereum.WcEthMethod
import com.tangem.domain.walletconnect.usecase.ethereum.WcEthMethod.SignMessage
import com.tangem.domain.walletconnect.model.WcEthMethod
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
@ -32,7 +31,7 @@ internal class WcEthNetwork(
val name = Name.entries.find { it.raw == methodName } ?: return null
return when (name) {
Name.Sign -> TODO()
Name.PersonalSign -> WcMethodHandler.fromJson<SignMessage>(params, moshi)
Name.PersonalSign -> TODO()
Name.SignTypeData -> TODO()
Name.SignTypeDataV4 -> TODO()
Name.SignTransaction -> TODO()
@ -43,7 +42,7 @@ internal class WcEthNetwork(
override fun handle(wcRequest: WcRequest<WcMethod>) {
wcRequest as WcRequest<WcEthMethod>
val useCase = when (wcRequest.method) {
is SignMessage -> TODO()
is WcEthMethod.PersonalEthSign -> TODO()
}
_useCases.trySend(useCase)
}

View file

@ -16,7 +16,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
class WcPairSdkDelegate : WcSdkObserver {
internal class WcPairSdkDelegate : WcSdkObserver {
private val onSessionProposal = Channel<Wallet.Model.SessionProposal>()
private val onSessionSettleResponse = Channel<Wallet.Model.SettledSessionResponse>()

View file

@ -13,7 +13,6 @@ import com.tangem.domain.walletconnect.model.WcSessionDTO
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
@ -30,7 +29,6 @@ internal class DefaultWcSessionsManager constructor(
private val store: WalletConnectStore,
private val legacyStore: WalletConnectSessionsRepository,
private val getWallets: GetWalletsUseCase,
private val getUserWallet: GetUserWalletUseCase,
private val dispatchers: CoroutineDispatcherProvider,
private val scope: CoroutineScope,
) : WcSessionsManager, WcSdkObserver {
@ -84,9 +82,12 @@ internal class DefaultWcSessionsManager constructor(
}
override suspend fun findSessionByTopic(topic: String): WcSession? = withContext(dispatchers.io) {
val storedSessions = store.findSessionByTopic(topic) ?: return@withContext null
val storedSessions = sessions.firstOrNull()
?.values?.flatten()
?.firstOrNull { it.sdkModel.topic == topic }
?: return@withContext null
val sdkSession = WalletKit.getActiveSessionByTopic(topic) ?: return@withContext null
val wallet = getUserWallet.invoke(storedSessions.walletId).getOrNull() ?: return@withContext null
val wallet = storedSessions.wallet
WcSession(wallet = wallet, sdkModel = WcSdkSessionConverter.convert(sdkSession))
}

View file

@ -3,6 +3,7 @@ package com.tangem.data.walletconnect.sign
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.data.walletconnect.respond.WcRespondService
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletconnect.usecase.WcMethodUseCase
import com.tangem.domain.walletconnect.usecase.sign.WcSignState
import com.tangem.domain.walletconnect.usecase.sign.WcSignUseCase
@ -17,6 +18,7 @@ internal abstract class BaseWcSignUseCase<MiddleAction, SignModel> :
abstract val respondService: WcRespondService
abstract val context: WcMethodUseCaseContext
override val network: Network get() = context.network
override val session: WcSession get() = context.session
override val rawSdkRequest: WcSdkSessionRequest get() = context.rawSdkRequest
@ -55,6 +57,7 @@ internal interface FinalActionCollector<SignModel> {
internal class WcMethodUseCaseContext(
val session: WcSession,
val rawSdkRequest: WcSdkSessionRequest,
val network: Network,
)
internal typealias OnSign<SignModel> =

View file

@ -6,6 +6,7 @@ plugins {
dependencies {
/* Project - Domain */
implementation(projects.domain.core)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.walletConnect.models)

View file

@ -0,0 +1,9 @@
package com.tangem.domain.walletconnect.model
sealed interface WcEthMethod : WcMethod {
data class PersonalEthSign(
val message: String,
val account: String,
) : WcEthMethod
}

View file

@ -1,5 +1,5 @@
package com.tangem.domain.walletconnect.model
interface WcMethod {
sealed interface WcMethod {
data object Unsupported : WcMethod
}

View file

@ -1,5 +1,6 @@
package com.tangem.domain.walletconnect.usecase
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
@ -8,4 +9,5 @@ interface WcUseCase
interface WcMethodUseCase : WcUseCase {
val session: WcSession
val rawSdkRequest: WcSdkSessionRequest
val network: Network
}

View file

@ -1,10 +0,0 @@
package com.tangem.domain.walletconnect.usecase.ethereum
import com.tangem.domain.walletconnect.model.WcMethod
sealed interface WcEthMethod : WcMethod {
data class SignMessage(
val raw: List<String>,
) : WcEthMethod
}

View file

@ -0,0 +1,15 @@
package com.tangem.domain.walletconnect.usecase.ethereum
import com.tangem.domain.walletconnect.usecase.sign.WcSignUseCase
interface WcPersonalEthSignUseCase :
WcSignUseCase,
WcSignUseCase.FinalAction,
WcSignUseCase.SimpleRun<WcPersonalEthSignUseCase.SignModel> {
data class SignModel(
val humanMsg: String,
val account: String,
val rawMsg: String,
)
}