Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-23 15:17:24 +07:00
parent 736ef3e45e
commit e87d946e40
10 changed files with 332 additions and 19 deletions

View file

@ -277,20 +277,39 @@ internal class DefaultTransactionRepository(
userWalletId: UserWalletId,
network: Network,
): Result<ByteArray> = withContext(coroutineDispatcherProvider.io) {
val blockchain = Blockchain.fromId(network.id.value)
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
val preparer = walletManager as? TransactionPreparer ?: kotlin.run {
Timber.e("${walletManager?.wallet?.blockchain} does not support TransactionBuilder")
error("Wallet manager does not support TransactionPreparer")
}
val preparer = getPreparer(network, userWalletId)
when (val prepareForSend = preparer.prepareForSend(transactionData, signer)) {
is com.tangem.blockchain.extensions.Result.Failure -> Result.failure(prepareForSend.error)
is com.tangem.blockchain.extensions.Result.Success -> Result.success(prepareForSend.data)
}
}
override suspend fun prepareForSendMultiple(
transactionData: List<TransactionData>,
signer: TransactionSigner,
userWalletId: UserWalletId,
network: Network,
): Result<List<ByteArray>> = withContext(coroutineDispatcherProvider.io) {
val preparer = getPreparer(network, userWalletId)
when (val prepareForSend = preparer.prepareForSendMultiple(transactionData, signer)) {
is com.tangem.blockchain.extensions.Result.Failure -> Result.failure(prepareForSend.error)
is com.tangem.blockchain.extensions.Result.Success -> Result.success(prepareForSend.data)
}
}
private suspend fun getPreparer(network: Network, userWalletId: UserWalletId): TransactionPreparer {
val blockchain = Blockchain.fromId(network.id.value)
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
val preparer = walletManager as? TransactionPreparer ?: run {
Timber.e("${walletManager?.wallet?.blockchain} does not support TransactionBuilder")
error("Wallet manager does not support TransactionPreparer")
}
return preparer
}
}

View file

@ -144,7 +144,15 @@ internal object WalletConnectDataModule {
@Provides
@Singleton
fun wcSolanaNetwork(excludedBlockchains: ExcludedBlockchains): WcSolanaNetwork = WcSolanaNetwork(
fun wcSolanaNetwork(
@SdkMoshi moshi: Moshi,
excludedBlockchains: ExcludedBlockchains,
sessionsManager: WcSessionsManager,
factories: WcSolanaNetwork.Factories,
): WcSolanaNetwork = WcSolanaNetwork(
moshi = moshi,
sessionsManager = sessionsManager,
factories = factories,
excludedBlockchains = excludedBlockchains,
)
@ -175,6 +183,7 @@ internal object WalletConnectDataModule {
fun diHelperBox(ethNetwork: WcEthNetwork, solanaNetwork: WcSolanaNetwork) = DiHelperBox(
handlers = setOf(
ethNetwork,
solanaNetwork,
),
converters = setOf(
ethNetwork,

View file

@ -0,0 +1,78 @@
package com.tangem.data.walletconnect.network.solana
import arrow.core.left
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.extensions.encodeBase64
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.domain.transaction.usecase.PrepareForSendUseCase
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.usecase.sign.WcSignState
import com.tangem.domain.walletconnect.usecase.solana.WcSolanaSignAllTransactionUseCase
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import okio.ByteString.Companion.decodeBase64
import org.json.JSONArray
import org.json.JSONObject
internal class DefaultWcSolanaSignAllTransactionUseCase @AssistedInject constructor(
override val respondService: WcRespondService,
private val prepareForSend: PrepareForSendUseCase,
@Assisted override val context: WcMethodUseCaseContext,
@Assisted private val method: WcSolanaMethod.SignAllTransaction,
) : BaseWcSignUseCase<Nothing, List<TransactionData.Compiled>>(),
WcSolanaSignAllTransactionUseCase {
override suspend fun SignCollector<List<TransactionData.Compiled>>.onSign(
state: WcSignState<List<TransactionData.Compiled>>,
) {
val hash = prepareForSend.invoke(transactionData = state.signModel, userWallet = wallet, network = network)
.onLeft { error ->
emit(state.toResult(error.left()))
}
.getOrNull()
?: return
val respond = getSolanaResultTxHashesString(hash)
val respondResult = respondService.respond(rawSdkRequest, respond)
emit(state.toResult(respondResult))
}
override fun invoke(): Flow<WcSignState<List<TransactionData.Compiled>>> {
val transactionData = method.transaction
.map { it.decodeBase64()?.toByteArray() ?: ByteArray(0) }
.map { TransactionData.Compiled(value = TransactionData.Compiled.Data.Bytes(it)) }
return delegate.invoke(transactionData)
}
/**
* Build json object
* {
* "transactions": [
* "signed_tx_hash"
* ]
* }
*/
private fun getSolanaResultTxHashesString(signedHashes: List<ByteArray>): String {
val result = JSONObject()
val transactions = JSONArray()
signedHashes.forEach {
transactions.put(it.encodeBase64())
}
result.put("transactions", transactions)
return result.toString()
}
@AssistedFactory
interface Factory {
fun create(
context: WcMethodUseCaseContext,
method: WcSolanaMethod.SignAllTransaction,
): DefaultWcSolanaSignAllTransactionUseCase
}
}

View file

@ -0,0 +1,57 @@
package com.tangem.data.walletconnect.network.solana
import arrow.core.left
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.extensions.encodeBase58
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.domain.transaction.usecase.PrepareForSendUseCase
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.usecase.sign.WcSignState
import com.tangem.domain.walletconnect.usecase.solana.WcSolanaSignTransactionUseCase
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import okio.ByteString.Companion.decodeBase64
internal class DefaultWcSolanaSignTransactionUseCase @AssistedInject constructor(
override val respondService: WcRespondService,
private val prepareForSend: PrepareForSendUseCase,
@Assisted override val context: WcMethodUseCaseContext,
@Assisted private val method: WcSolanaMethod.SignTransaction,
) : BaseWcSignUseCase<Nothing, TransactionData.Compiled>(),
WcSolanaSignTransactionUseCase {
override suspend fun SignCollector<TransactionData.Compiled>.onSign(state: WcSignState<TransactionData.Compiled>) {
val hash = prepareForSend.invoke(transactionData = state.signModel, userWallet = wallet, network = network)
.onLeft { error ->
emit(state.toResult(error.left()))
}
.getOrNull()
?: return
val respond = "{ signature: \"${hash.encodeBase58()}\" }"
val respondResult = respondService.respond(rawSdkRequest, respond)
emit(state.toResult(respondResult))
}
override fun invoke(): Flow<WcSignState<TransactionData.Compiled>> {
val data = method.transaction.decodeBase64()?.toByteArray() ?: ByteArray(0)
val transactionData = TransactionData.Compiled(
value = TransactionData.Compiled.Data.Bytes(data),
)
return delegate.invoke(transactionData)
}
@AssistedFactory
interface Factory {
fun create(
context: WcMethodUseCaseContext,
method: WcSolanaMethod.SignTransaction,
): DefaultWcSolanaSignTransactionUseCase
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.data.walletconnect.network.solana
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class WcSolanaSignMessageRequest(
@Json(name = "pubkey")
val publicKey: String,
@Json(name = "message")
val message: String,
)
@JsonClass(generateAdapter = true)
internal data class WcSolanaSignTransactionRequest(
@Json(name = "transaction")
val transaction: String,
)

View file

@ -1,19 +1,45 @@
package com.tangem.data.walletconnect.network.solana
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
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
import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext
import com.tangem.data.walletconnect.utils.WcNamespaceConverter
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.repository.WcSessionsManager
import com.tangem.domain.walletconnect.usecase.WcMethodUseCase
import com.tangem.domain.wallets.models.UserWallet
import jakarta.inject.Inject
internal class WcSolanaNetwork constructor(
private val moshi: Moshi,
private val sessionsManager: WcSessionsManager,
private val factories: Factories,
private val excludedBlockchains: ExcludedBlockchains,
) : WcNamespaceConverter {
) : WcNamespaceConverter, WcRequestToUseCaseConverter {
override val namespaceKey: NamespaceKey = NamespaceKey("solana")
override suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase? {
val methodKey = request.request.method
val name = Name.entries.find { it.raw == methodKey } ?: return null
val method: WcSolanaMethod = name.toMethod(request) ?: return null
val session = sessionsManager.findSessionByTopic(request.topic) ?: return null
val network = toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null
val context = WcMethodUseCaseContext(session = session, rawSdkRequest = request, network = network)
return when (method) {
is WcSolanaMethod.SignMessage -> TODO()
is WcSolanaMethod.SignTransaction -> factories.signTransaction.create(context, method)
is WcSolanaMethod.SignAllTransaction -> factories.signAllTransaction.create(context, method)
}
}
override fun toBlockchain(chainId: CAIP2): Blockchain? {
if (chainId.namespace != namespaceKey.key) return null
return when (chainId.reference) {
@ -41,6 +67,32 @@ internal class WcSolanaNetwork constructor(
)
}
private fun Name.toMethod(request: WcSdkSessionRequest): WcSolanaMethod? {
val rawParams = request.request.params
return when (this) {
Name.SignMessage -> moshi.fromJson<WcSolanaSignMessageRequest>(rawParams)?.let { request ->
WcSolanaMethod.SignMessage(pubKey = request.publicKey, message = request.message)
}
Name.SignTransaction -> moshi.fromJson<WcSolanaSignTransactionRequest>(rawParams)?.let { request ->
WcSolanaMethod.SignTransaction(request.transaction)
}
Name.SendAllTransaction -> moshi.fromJson<List<String>>(rawParams)?.let { list ->
WcSolanaMethod.SignAllTransaction(list)
}
}
}
private enum class Name(val raw: String) {
SignMessage("solana_signMessage"),
SignTransaction("solana_signTransaction"),
SendAllTransaction("solana_signAllTransactions"),
}
internal class Factories @Inject constructor(
val signTransaction: DefaultWcSolanaSignTransactionUseCase.Factory,
val signAllTransaction: DefaultWcSolanaSignAllTransactionUseCase.Factory,
)
companion object {
private const val MAINNET_CHAIN_ID = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
private const val TESTNET_CHAIN_ID = "4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z"

View file

@ -89,4 +89,11 @@ interface TransactionRepository {
userWalletId: UserWalletId,
network: Network,
): Result<ByteArray>
suspend fun prepareForSendMultiple(
transactionData: List<TransactionData>,
signer: TransactionSigner,
userWalletId: UserWalletId,
network: Network,
): Result<List<ByteArray>>
}

View file

@ -4,6 +4,7 @@ import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.domain.card.models.TwinKey
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
@ -20,13 +21,7 @@ class PrepareForSendUseCase(
userWallet: UserWallet,
network: Network,
): Either<Throwable, ByteArray> {
val card = userWallet.scanResponse.card
val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins
val signer = cardSdkConfigRepository.getCommonSigner(
cardId = card.cardId.takeIf { isCardNotBackedUp },
twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse),
)
val signer = createSigner(userWallet)
return transactionRepository.prepareForSend(
transactionData = transactionData,
userWalletId = userWallet.walletId,
@ -35,4 +30,29 @@ class PrepareForSendUseCase(
)
.fold(onSuccess = { it.right() }, onFailure = { it.left() })
}
suspend operator fun invoke(
transactionData: List<TransactionData>,
userWallet: UserWallet,
network: Network,
): Either<Throwable, List<ByteArray>> {
val signer = createSigner(userWallet)
return transactionRepository.prepareForSendMultiple(
transactionData = transactionData,
userWalletId = userWallet.walletId,
network = network,
signer = signer,
)
.fold(onSuccess = { it.right() }, onFailure = { it.left() })
}
private fun createSigner(userWallet: UserWallet): TransactionSigner {
val card = userWallet.scanResponse.card
val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins
val signer = cardSdkConfigRepository.getCommonSigner(
cardId = card.cardId.takeIf { isCardNotBackedUp },
twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse),
)
return signer
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.walletconnect.model
sealed interface WcSolanaMethod : WcMethod {
data class SignMessage(
val pubKey: String,
val message: String,
) : WcSolanaMethod
data class SignTransaction(
val transaction: String,
) : WcSolanaMethod
data class SignAllTransaction(
val transaction: List<String>,
) : WcSolanaMethod
}

View file

@ -0,0 +1,35 @@
package com.tangem.domain.walletconnect.usecase.solana
import com.tangem.blockchain.common.TransactionData
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.usecase.sign.WcSignState
import com.tangem.domain.walletconnect.usecase.sign.WcSignUseCase
import kotlinx.coroutines.flow.Flow
interface WcSolanaSignTransactionUseCase :
WcSignUseCase,
WcSignUseCase.SimpleRun<TransactionData.Compiled> {
override val network: Network
override val rawSdkRequest: WcSdkSessionRequest
override val session: WcSession
override fun sign()
override fun cancel()
override fun invoke(): Flow<WcSignState<TransactionData.Compiled>>
}
interface WcSolanaSignAllTransactionUseCase :
WcSignUseCase,
WcSignUseCase.SimpleRun<List<TransactionData.Compiled>> {
override val network: Network
override val rawSdkRequest: WcSdkSessionRequest
override val session: WcSession
override fun sign()
override fun cancel()
override fun invoke(): Flow<WcSignState<List<TransactionData.Compiled>>>
}