diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/Model.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/Model.kt index fcd037a22a..252610e479 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/Model.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/Model.kt @@ -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") diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt index 0265091789..479ff8a0c5 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt @@ -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(rawParams) .getOrElse { return it.left() } - ?.let { request -> WcSolanaMethod.SignTransaction(request.transaction, request.feePayer) } + ?.let { request -> SignTransaction(request.transaction, request.feePayer) } WcSolanaMethodName.SendAllTransaction -> moshi.fromJson(rawParams) .getOrElse { return it.left() } - ?.let { request -> WcSolanaMethod.SignAllTransaction(request.transactions) } + ?.let { request -> SignAllTransaction(request.transactions) } + WcSolanaMethodName.SignAndSendTransaction -> moshi.fromJson( + 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 { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignAndSendTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignAndSendTransactionUseCase.kt new file mode 100644 index 0000000000..897db70e35 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignAndSendTransactionUseCase.kt @@ -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(), + 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.onSign(state: WcSignState) { + 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> { + 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 + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt index 2f3ddc858d..bcd5f64cb5 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt @@ -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, -> { diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt index 982c1cacf1..7b5ee46bd2 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt @@ -21,4 +21,5 @@ enum class WcSolanaMethodName(override val raw: String) : WcMethodName { SignMessage("solana_signMessage"), SignTransaction("solana_signTransaction"), SendAllTransaction("solana_signAllTransactions"), + SignAndSendTransaction("solana_signAndSendTransaction"), } \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt index d44d98c88f..eef1aa5827 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt @@ -20,6 +20,12 @@ sealed interface WcSolanaMethod : WcMethod { override val methodName: String = WcSolanaMethodName.SignTransaction.raw } + data class SignAndSendTransaction( + val transaction: String, + ) : WcSolanaMethod { + override val methodName: String = WcSolanaMethodName.SignAndSendTransaction.raw + } + data class SignAllTransaction( val transaction: List, ) : WcSolanaMethod { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt index ddfb9f8b3b..be05aa7aa2 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt @@ -62,6 +62,7 @@ internal class WcRoutingModel @Inject constructor( WcEthMethodName.SignTransaction, WcEthMethodName.SendTransaction, WcSolanaMethodName.SignTransaction, + WcSolanaMethodName.SignAndSendTransaction, WcSolanaMethodName.SendAllTransaction, WcBitcoinMethodName.SendTransfer, WcBitcoinMethodName.SignPsbt, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt index f1b56b37bc..b51f19d1bd 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt @@ -2,8 +2,10 @@ 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.models.wallet.isHotWallet 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.WcSolanaMethod import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck import com.tangem.domain.walletconnect.usecase.method.WcMethodContext @@ -17,7 +19,6 @@ import com.tangem.features.walletconnect.transaction.entity.common.WcTransaction import com.tangem.features.walletconnect.transaction.entity.send.WcSendTransactionItemUM import com.tangem.features.walletconnect.transaction.entity.send.WcSendTransactionUM import com.tangem.features.walletconnect.utils.WcNotificationsFactory -import com.tangem.domain.models.wallet.isHotWallet import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject @@ -42,6 +43,7 @@ internal class WcSendTransactionUMConverter @Inject constructor( is WcSolanaMethod.SignTransaction, is WcBitcoinMethod.SendTransfer, is WcBitcoinMethod.SignPsbt, + is WcSolanaMethod.SignAndSendTransaction, is WcBitcoinMethod.SignMessage, -> WcSendTransactionUM( transaction = WcSendTransactionItemUM( @@ -84,7 +86,14 @@ internal class WcSendTransactionUMConverter @Inject constructor( onCopy = value.actions.onCopy, ), ) - else -> null + is WcBitcoinMethod.GetAccountAddresses, + is WcEthMethod.AddEthereumChain, + is WcEthMethod.MessageSign, + is WcEthMethod.SignTypedData, + is WcEthMethod.SwitchEthereumChain, + is WcMethod.Unsupported, + is WcSolanaMethod.SignMessage, + -> null } }