Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-18 22:03:10 +07:00
parent 6373b5a8a3
commit c1ad1e43ce
8 changed files with 127 additions and 57 deletions

View file

@ -1,5 +1,9 @@
package com.tangem.data.walletconnect.network.ethereum
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
@ -35,12 +39,18 @@ internal class WcEthNetwork(
}
@Suppress("CyclomaticComplexMethod")
override suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase? {
val name = toWcMethodName(request) ?: return null
val session = sessionsManager.findSessionByTopic(request.topic) ?: return null
override suspend fun toUseCase(
request: WcSdkSessionRequest,
): Either<WcRequestError.HandleMethodError, WcMethodUseCase> {
fun error(message: String) = WcRequestError.HandleMethodError(message).left()
val name = toWcMethodName(request) ?: return error("Unknown method name")
val session = sessionsManager.findSessionByTopic(request.topic)
?: return error("Failed to find session for topic ${request.topic}")
val wallet = session.wallet
val chainId = request.chainId.orEmpty()
val method: WcEthMethod = name.toMethod(request, wallet) ?: return null
val method: WcEthMethod = name.toMethod(request, wallet)
.getOrElse { return error(it.message.orEmpty()) }
?: return error("Failed to parse $name")
suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet)
val accountAddress = when (method) {
@ -60,7 +70,7 @@ internal class WcEthNetwork(
is WcEthMethod.SignTransaction,
-> networksConverter.findWalletNetworkForRequest(request, session, accountAddress)
is WcEthMethod.AddEthereumChain -> anyExistNetwork()
} ?: return null
} ?: return error("Failed to find walletNetwork for accountAddress $accountAddress")
val context = WcMethodUseCaseContext(
session = session,
@ -74,10 +84,13 @@ internal class WcEthNetwork(
is WcEthMethod.SignTransaction -> factories.signTransaction.create(context, method)
is WcEthMethod.SignTypedData -> factories.signTypedData.create(context, method)
is WcEthMethod.AddEthereumChain -> factories.addNetwork.create(context, method)
}
}.right()
}
private suspend fun WcEthMethodName.toMethod(request: WcSdkSessionRequest, wallet: UserWallet): WcEthMethod? {
private suspend fun WcEthMethodName.toMethod(
request: WcSdkSessionRequest,
wallet: UserWallet,
): Either<Throwable, WcEthMethod?> {
val rawParams = request.request.params
return when (this) {
WcEthMethodName.EthSign,
@ -89,39 +102,48 @@ internal class WcEthNetwork(
WcEthMethodName.SignTransaction,
WcEthMethodName.SendTransaction,
-> moshi.fromJson<List<WcEthTransactionParams>>(rawParams)
.getOrElse { return it.left() }
?.firstOrNull()
?.let {
if (this == WcEthMethodName.SignTransaction) {
WcEthMethod.SignTransaction(transaction = it)
WcEthMethod.SignTransaction(transaction = it).right()
} else {
WcEthMethod.SendTransaction(transaction = it)
WcEthMethod.SendTransaction(transaction = it).right()
}
}
?: return null.right()
WcEthMethodName.AddEthereumChain -> moshi.fromJson<List<WcEthAddChain>>(rawParams)
.getOrElse { return it.left() }
?.firstOrNull()
?.let {
val newNetwork = networksConverter.mainOrAnyWalletNetworkForRequest(it.chainId, wallet)
?: return null
WcEthMethod.AddEthereumChain(rawChain = it, network = newNetwork)
val newNetwork = networksConverter
.mainOrAnyWalletNetworkForRequest(it.chainId, wallet)
?: return null.right()
WcEthMethod.AddEthereumChain(rawChain = it, network = newNetwork).right()
}
?: null.right()
}
}
private fun WcEthMethodName.parseMessageSign(rawParams: String): WcEthMethod.MessageSign? {
val list = moshi.fromJson<List<String>>(rawParams) ?: return null
private fun WcEthMethodName.parseMessageSign(rawParams: String): Either<Throwable, WcEthMethod.MessageSign?> {
val list = moshi.fromJson<List<String>>(rawParams)
.getOrElse { return it.left() }
?: return null.right()
val accountIndex = if (this == WcEthMethodName.EthSign) 0 else 1
val messageIndex = if (this == WcEthMethodName.EthSign) 1 else 0
val account = list.getOrNull(accountIndex) ?: return null
val message = list.getOrNull(messageIndex) ?: return null
val account = list.getOrNull(accountIndex) ?: return null.right()
val message = list.getOrNull(messageIndex) ?: return null.right()
val humanMsg = LegacySdkHelper.hexToAscii(message).orEmpty()
return WcEthMethod.MessageSign(account = account, rawMessage = message, humanMsg = humanMsg)
return WcEthMethod.MessageSign(account = account, rawMessage = message, humanMsg = humanMsg).right()
}
private fun parseTypeData(params: String): WcEthMethod.SignTypedData? {
private fun parseTypeData(params: String): Either<Throwable, WcEthMethod.SignTypedData?> {
val account = params.substring(params.indexOf("\"") + 1, params.indexOf("\"", startIndex = 2))
val data = params.substring(params.indexOfFirst { it == '{' }, params.indexOfLast { it == '}' } + 1)
val parsedParams = moshi.fromJson<WcEthSignTypedDataParams>(data) ?: return null
return WcEthMethod.SignTypedData(params = parsedParams, account = account, dataForSign = data)
val parsedParams = moshi.fromJson<WcEthSignTypedDataParams>(data)
.getOrElse { return it.left() }
?: return null.right()
return WcEthMethod.SignTypedData(params = parsedParams, account = account, dataForSign = data).right()
}
internal class NamespaceConverter(

View file

@ -1,11 +1,16 @@
package com.tangem.data.walletconnect.network.solana
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.blockchain.extensions.decodeBase58
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.extensions.toHexString
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
@ -13,7 +18,7 @@ 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.models.network.Network
import com.tangem.data.walletconnect.model.CAIP2
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.model.WcSolanaMethodName
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
@ -37,10 +42,16 @@ internal class WcSolanaNetwork(
}
@Suppress("CyclomaticComplexMethod")
override suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase? {
val name = toWcMethodName(request) ?: return null
val method: WcSolanaMethod = name.toMethod(request) ?: return null
val session = sessionsManager.findSessionByTopic(request.topic) ?: return null
override suspend fun toUseCase(
request: WcSdkSessionRequest,
): Either<WcRequestError.HandleMethodError, WcMethodUseCase> {
fun error(message: String) = WcRequestError.HandleMethodError(message).left()
val name = toWcMethodName(request) ?: return error("Unknown method name")
val method: WcSolanaMethod = name.toMethod(request)
.getOrElse { return error(it.message.orEmpty()) }
?: return error("Failed to parse $name")
val session = sessionsManager.findSessionByTopic(request.topic)
?: return error("Failed to find session for topic ${request.topic}")
val wallet = session.wallet
val chainId = request.chainId.orEmpty()
suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet)
@ -56,7 +67,7 @@ internal class WcSolanaNetwork(
val walletNetwork = networksConverter
.findWalletNetworkForRequest(request, session, accountAddress)
?: anyExistNetwork()
?: return null
?: return error("Failed to find walletNetwork for accountAddress $accountAddress")
val context = WcMethodUseCaseContext(
session = session,
@ -68,7 +79,7 @@ internal class WcSolanaNetwork(
is WcSolanaMethod.SignMessage -> factories.messageSign.create(context, method)
is WcSolanaMethod.SignTransaction -> factories.signTransaction.create(context, method)
is WcSolanaMethod.SignAllTransaction -> factories.signAllTransaction.create(context, method)
}
}.right()
}
internal class NamespaceConverter @Inject constructor(
@ -101,23 +112,26 @@ internal class WcSolanaNetwork(
}
}
private fun WcSolanaMethodName.toMethod(request: WcSdkSessionRequest): WcSolanaMethod? {
private fun WcSolanaMethodName.toMethod(request: WcSdkSessionRequest): Either<Throwable, WcSolanaMethod?> {
val rawParams = request.request.params
return when (this) {
WcSolanaMethodName.SignMessage -> moshi.fromJson<WcSolanaSignMessageRequest>(rawParams)?.let { request ->
val humanMsg = request.message.decodeBase58()?.toHexString().orEmpty()
WcSolanaMethod.SignMessage(
pubKey = request.publicKey,
rawMessage = request.message,
humanMsg = humanMsg,
)
}
WcSolanaMethodName.SignMessage -> moshi.fromJson<WcSolanaSignMessageRequest>(rawParams)
.getOrElse { return it.left() }
?.let { request ->
val humanMsg = request.message.decodeBase58()?.toHexString().orEmpty()
WcSolanaMethod.SignMessage(
pubKey = request.publicKey,
rawMessage = request.message,
humanMsg = humanMsg,
)
}
WcSolanaMethodName.SignTransaction -> moshi.fromJson<WcSolanaSignTransactionRequest>(rawParams)
.getOrElse { return it.left() }
?.let { request -> WcSolanaMethod.SignTransaction(request.transaction, request.feePayer) }
WcSolanaMethodName.SendAllTransaction -> moshi.fromJson<List<String>>(rawParams)?.let { list ->
WcSolanaMethod.SignAllTransaction(list)
}
}
WcSolanaMethodName.SendAllTransaction -> moshi.fromJson<List<String>>(rawParams)
.getOrElse { return it.left() }
?.let { list -> WcSolanaMethod.SignAllTransaction(list) }
}.right()
}
internal class Factories @Inject constructor(

View file

@ -3,10 +3,13 @@ package com.tangem.data.walletconnect.request
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.data.walletconnect.utils.WC_TAG
import com.tangem.domain.walletconnect.WcRequestUseCaseFactory
import com.tangem.domain.walletconnect.model.WcMethod
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase
import timber.log.Timber
import javax.inject.Inject
internal class DefaultWcRequestUseCaseFactory @Inject constructor(
@ -17,7 +20,17 @@ internal class DefaultWcRequestUseCaseFactory @Inject constructor(
override suspend fun <T : WcMethodUseCase> createUseCase(
request: WcSdkSessionRequest,
): Either<WcMethod.Unsupported, T> {
val useCase = requestConverters.firstNotNullOfOrNull { converter -> converter.toUseCase(request) }
return (useCase as? T)?.let { useCase.right() } ?: WcMethod.Unsupported(request).left()
val useCase = requestConverters
.find { it.toWcMethodName(request) != null }
?.toUseCase(request)
?: WcRequestError.HandleMethodError("Failed to create WcUseCase").left()
return useCase.fold(
ifLeft = {
Timber.tag(WC_TAG).e("$it")
WcMethod.Unsupported(request).left()
},
ifRight = { (it as? T)?.right() ?: WcMethod.Unsupported(request).left() },
)
}
}

View file

@ -1,18 +1,27 @@
package com.tangem.data.walletconnect.request
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.tangem.domain.walletconnect.model.WcMethodName
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase
interface WcRequestToUseCaseConverter {
fun toWcMethodName(request: WcSdkSessionRequest): WcMethodName?
suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase?
suspend fun toUseCase(request: WcSdkSessionRequest): Either<WcRequestError.HandleMethodError, WcMethodUseCase>
companion object {
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified T> Moshi.fromJson(params: String): T? =
runCatching { this.adapter<T>().fromJson(params) }.getOrNull()
inline fun <reified T> Moshi.fromJson(params: String): Either<Throwable, T?> {
return runCatching { this.adapter<T>().fromJson(params) }
.fold(
onSuccess = { it.right() },
onFailure = { it.left() },
)
}
}
}

View file

@ -26,13 +26,18 @@ internal class DefaultWcRespondService : WcRespondService {
),
onSuccess = {
if (continuation.isCompleted) return@respondSessionRequest
Timber.tag(WC_TAG).i("Successful respond for request $request")
val result = when (val response = it.jsonRpcResponse) {
is Wallet.Model.JsonRpcResponse.JsonRpcError -> WcRequestError.WcRespondError(
code = response.code,
message = response.message,
).left()
is Wallet.Model.JsonRpcResponse.JsonRpcResult -> response.result.right()
is Wallet.Model.JsonRpcResponse.JsonRpcError -> {
Timber.tag(WC_TAG).e("Failed respond $response for request $request")
WcRequestError.WcRespondError(
code = response.code,
message = response.message,
).left()
}
is Wallet.Model.JsonRpcResponse.JsonRpcResult -> {
Timber.tag(WC_TAG).i("Successful respond $response for request $request")
response.result.right()
}
}
continuation.resume(result)
},

View file

@ -15,7 +15,7 @@ sealed interface WcEthMethod : WcMethod {
val account: String,
val dataForSign: String,
) : WcEthMethod {
val humanMsg: String = params.message.contents
val humanMsg: String = params.message.contents.orEmpty()
}
data class SendTransaction(

View file

@ -12,7 +12,7 @@ data class WcEthSignTypedDataParams(
@Json(name = "primaryType")
val primaryType: String,
@Json(name = "types")
val types: Types,
val types: Map<String, List<Types.Type>>,
) {
@JsonClass(generateAdapter = true)
data class Domain(
@ -29,11 +29,11 @@ data class WcEthSignTypedDataParams(
@JsonClass(generateAdapter = true)
data class Message(
@Json(name = "contents")
val contents: String,
val contents: String?,
@Json(name = "from")
val from: Address,
val from: Address?,
@Json(name = "to")
val to: Address,
val to: Address?,
) {
@JsonClass(generateAdapter = true)
data class Address(

View file

@ -13,6 +13,10 @@ sealed class WcRequestError {
val message: String,
) : WcRequestError()
data class HandleMethodError(
val message: String,
) : WcRequestError()
data class UnknownError(val ex: Throwable? = null) : WcRequestError()
companion object {
@ -21,10 +25,13 @@ sealed class WcRequestError {
is UnknownError -> ex?.message
is WcRespondError -> message
is WrappedSendError -> sendTransactionError.message()
is HandleMethodError -> message
}
fun WcRequestError.code(): String? = when (this) {
is UnknownError -> null
is HandleMethodError,
is UnknownError,
-> null
is WcRespondError -> this.code.toString()
is WrappedSendError -> sendTransactionError.code()
}