Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-08 12:38:27 +04:00
parent 54739fff06
commit bd06d03571
8 changed files with 184 additions and 11 deletions

View file

@ -21,6 +21,12 @@ internal data class WcSolanaSignTransactionRequest(
val feePayer: String?,
)
@JsonClass(generateAdapter = true)
internal data class WcSolanaSignAndSendTransactionRequest(
@Json(name = "transaction")
val transaction: String,
)
@JsonClass(generateAdapter = true)
internal data class WcSolanaSignAllTransactionRequest(
@Json(name = "transactions")

View file

@ -18,6 +18,7 @@ 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.WcSolanaMethod
import com.tangem.domain.walletconnect.model.WcSolanaMethod.*
import com.tangem.domain.walletconnect.model.WcSolanaMethodName
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.repository.WcSessionsManager
@ -55,9 +56,11 @@ internal class WcSolanaNetwork(
.orEmpty()
val accountAddress = when (method) {
is WcSolanaMethod.SignAllTransaction -> anyAddress()
is WcSolanaMethod.SignMessage -> anyAddress()
is WcSolanaMethod.SignTransaction -> method.address ?: anyAddress()
is SignAllTransaction,
is SignMessage,
is SignAndSendTransaction,
-> anyAddress()
is SignTransaction -> method.address ?: anyAddress()
}
val walletNetwork = networksConverter
.findWalletNetworkForRequest(request, session, accountAddress)
@ -73,9 +76,10 @@ internal class WcSolanaNetwork(
networkDerivationsCount = networkDerivationsCount,
)
return when (method) {
is WcSolanaMethod.SignMessage -> factories.messageSign.create(context, method)
is WcSolanaMethod.SignTransaction -> factories.signTransaction.create(context, method)
is WcSolanaMethod.SignAllTransaction -> factories.signAllTransaction.create(context, method)
is SignMessage -> factories.messageSign.create(context, method)
is SignTransaction -> factories.signTransaction.create(context, method)
is SignAllTransaction -> factories.signAllTransaction.create(context, method)
is SignAndSendTransaction -> factories.signAndSendTransaction.create(context, method)
}.right()
}
@ -103,7 +107,7 @@ internal class WcSolanaNetwork(
.getOrElse { return it.left() }
?.let { request ->
val humanMsg = request.message.decodeBase58()?.toHexString().orEmpty()
WcSolanaMethod.SignMessage(
SignMessage(
pubKey = request.publicKey,
rawMessage = request.message,
humanMsg = humanMsg,
@ -111,10 +115,15 @@ internal class WcSolanaNetwork(
}
WcSolanaMethodName.SignTransaction -> moshi.fromJson<WcSolanaSignTransactionRequest>(rawParams)
.getOrElse { return it.left() }
?.let { request -> WcSolanaMethod.SignTransaction(request.transaction, request.feePayer) }
?.let { request -> SignTransaction(request.transaction, request.feePayer) }
WcSolanaMethodName.SendAllTransaction -> moshi.fromJson<WcSolanaSignAllTransactionRequest>(rawParams)
.getOrElse { return it.left() }
?.let { request -> WcSolanaMethod.SignAllTransaction(request.transactions) }
?.let { request -> SignAllTransaction(request.transactions) }
WcSolanaMethodName.SignAndSendTransaction -> moshi.fromJson<WcSolanaSignAndSendTransactionRequest>(
rawParams,
)
.getOrElse { return it.left() }
?.let { request -> SignAndSendTransaction(request.transaction) }
}.right()
}
@ -122,6 +131,7 @@ internal class WcSolanaNetwork(
val messageSign: WcSolanaMessageSignUseCase.Factory,
val signTransaction: WcSolanaSignTransactionUseCase.Factory,
val signAllTransaction: WcSolanaSignAllTransactionUseCase.Factory,
val signAndSendTransaction: WcSolanaSignAndSendTransactionUseCase.Factory,
)
companion object {

View file

@ -0,0 +1,139 @@
package com.tangem.data.walletconnect.network.solana
import arrow.core.left
import com.tangem.blockchain.blockchains.solana.SolanaTransactionHelper
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.extensions.decodeBase58
import com.tangem.blockchain.extensions.encodeBase58
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.models.wallet.UserWallet
import com.tangem.domain.transaction.usecase.SendLargeSolanaTransactionUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.walletconnect.WcAnalyticEvents.SolanaLargeTransactionStatus
import com.tangem.domain.walletconnect.error.parseSendError
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck
import com.tangem.domain.walletconnect.usecase.method.SignRequirements
import com.tangem.domain.walletconnect.usecase.method.WcSignState
import com.tangem.domain.walletconnect.usecase.method.WcTransactionUseCase
import com.tangem.lib.crypto.BlockchainUtils.SOLANA_TRANSACTION_SIZE_THRESHOLD_BYTES
import com.tangem.utils.logging.TangemLogger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@Suppress("LongParameterList")
internal class WcSolanaSignAndSendTransactionUseCase @AssistedInject constructor(
override val respondService: WcRespondService,
override val analytics: AnalyticsEventHandler,
private val sendTransaction: SendTransactionUseCase,
private val sendLargeSolanaTransactionUseCase: SendLargeSolanaTransactionUseCase,
@Assisted override val context: WcMethodUseCaseContext,
@Assisted override val method: WcSolanaMethod.SignAndSendTransaction,
blockAidDelegate: BlockAidVerificationDelegate,
addressConverter: SolanaBlockAidAddressConverter,
) : BaseWcSignUseCase<Nothing, TransactionData>(),
WcTransactionUseCase,
SignRequirements {
override val securityStatus = blockAidDelegate.getSecurityStatus(
network = network,
method = method,
rawSdkRequest = rawSdkRequest,
session = session,
accountAddress = addressConverter.convert(context.accountAddress),
).map { lce -> lce.map { result -> BlockAidTransactionCheck.Result.Plain(result) } }
override suspend fun SignCollector<TransactionData>.onSign(state: WcSignState<TransactionData>) {
val hash = state.signModel.getTxHashFromCompiled()
val formattedHash = getFormattedHash(hash) // uses for flow sendLargeSolanaTransaction
if (context.session.wallet is UserWallet.Cold && isLargeHash(formattedHash)) {
// workaround for large transactions that cannot be signed directly by card
TangemLogger.w("The transaction hash is too large to be signed directly: ${formattedHash.size} bytes")
sendLargeSolanaTransactionUseCase(context.session.wallet as UserWallet.Cold, context.network, formattedHash)
.fold(
ifLeft = { error ->
analytics.send(SolanaLargeTransactionStatus(SolanaLargeTransactionStatus.Status.Failed))
TangemLogger.e(error.toString())
emit(state.toResult(parseSendError(error).left()))
},
ifRight = {
analytics.send(SolanaLargeTransactionStatus(SolanaLargeTransactionStatus.Status.Success))
val emptyRespond = ByteArray(0).formatAsSolanaSignature()
val respondResult = respondService.respond(rawSdkRequest, emptyRespond)
emit(state.toResult(respondResult))
},
)
} else {
val signedHash =
sendTransaction.invoke(txData = state.signModel, userWallet = wallet, network = network)
.onLeft { error ->
emit(state.toResult(parseSendError(error).left()))
}
.getOrNull()
?: return
val respond = signedHash.formatAsSolanaSignature()
val respondResult = respondService.respond(rawSdkRequest, respond)
emit(state.toResult(respondResult))
}
}
override fun invoke(): Flow<WcSignState<TransactionData>> {
val data = method.transaction.decodeBase58() ?: byteArrayOf()
val transactionData = TransactionData.Compiled(
value = TransactionData.Compiled.Data.Bytes(data),
)
return delegate.invoke(transactionData)
}
private fun String.formatAsSolanaSignature(): String {
return "{ signature: \"${this}\" }"
}
private fun ByteArray.formatAsSolanaSignature(): String {
return "{ signature: \"${this.encodeBase58()}\" }"
}
private fun TransactionData.getTxHashFromCompiled(): ByteArray {
return when (this) {
is TransactionData.Compiled -> (value as? TransactionData.Compiled.Data.Bytes)?.data
?: error("Invalid transaction data")
is TransactionData.Uncompiled -> error("Transaction must be compiled")
}
}
private fun isLargeHash(hash: ByteArray): Boolean {
return hash.size > SOLANA_TRANSACTION_SIZE_THRESHOLD_BYTES
}
private fun getFormattedHash(hash: ByteArray): ByteArray {
return try {
SolanaTransactionHelper.removeSignaturesPlaceholders(hash)
} catch (e: Exception) {
TangemLogger.e("Failed to format the hash: ${e.message}")
hash
}
}
override fun isMultipleSignRequired(): Boolean {
val data = method.transaction.decodeBase58() ?: byteArrayOf()
return isLargeHash(data)
}
@AssistedFactory
interface Factory {
fun create(
context: WcMethodUseCaseContext,
method: WcSolanaMethod.SignAndSendTransaction,
): WcSolanaSignAndSendTransactionUseCase
}
}

View file

@ -53,6 +53,7 @@ internal class BlockAidVerificationDelegate @Inject constructor(
is WcEthMethod -> TransactionParams.Evm(rawSdkRequest.request.params)
is WcSolanaMethod.SignAllTransaction -> TransactionParams.Solana(method.transaction)
is WcSolanaMethod.SignTransaction -> TransactionParams.Solana(listOf(method.transaction))
is WcSolanaMethod.SignAndSendTransaction -> TransactionParams.Solana(listOf(method.transaction))
is WcSolanaMethod.SignMessage,
is WcBitcoinMethod,
-> {