diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index da6951cafd..7c4b516b71 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -277,20 +277,39 @@ internal class DefaultTransactionRepository( userWalletId: UserWalletId, network: Network, ): Result = 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, + signer: TransactionSigner, + userWalletId: UserWalletId, + network: Network, + ): Result> = 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 + } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index 385089f187..3c2b20e4f5 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -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, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignAllTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignAllTransactionUseCase.kt new file mode 100644 index 0000000000..4724192a80 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignAllTransactionUseCase.kt @@ -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>(), + WcSolanaSignAllTransactionUseCase { + + override suspend fun SignCollector>.onSign( + state: WcSignState>, + ) { + 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>> { + 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): 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 + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignTransactionUseCase.kt new file mode 100644 index 0000000000..4a497421ff --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignTransactionUseCase.kt @@ -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(), + WcSolanaSignTransactionUseCase { + + override suspend fun SignCollector.onSign(state: WcSignState) { + 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> { + 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 + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..fe252ca79c --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/Model.kt @@ -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, +) \ No newline at end of file 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 d4c72c3ee2..d376a58417 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 @@ -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(rawParams)?.let { request -> + WcSolanaMethod.SignMessage(pubKey = request.publicKey, message = request.message) + } + Name.SignTransaction -> moshi.fromJson(rawParams)?.let { request -> + WcSolanaMethod.SignTransaction(request.transaction) + } + Name.SendAllTransaction -> moshi.fromJson>(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" diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 9ef573a666..30ab51adf6 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -89,4 +89,11 @@ interface TransactionRepository { userWalletId: UserWalletId, network: Network, ): Result + + suspend fun prepareForSendMultiple( + transactionData: List, + signer: TransactionSigner, + userWalletId: UserWalletId, + network: Network, + ): Result> } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt index 58d073be30..b2e29146b2 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt @@ -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 { - 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, + userWallet: UserWallet, + network: Network, + ): Either> { + 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 + } } \ 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 new file mode 100644 index 0000000000..3bc1f2dfbc --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt @@ -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, + ) : WcSolanaMethod +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/solana/WcSolanaSignTransactionUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/solana/WcSolanaSignTransactionUseCase.kt new file mode 100644 index 0000000000..92eb17320f --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/solana/WcSolanaSignTransactionUseCase.kt @@ -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 { + + override val network: Network + override val rawSdkRequest: WcSdkSessionRequest + override val session: WcSession + + override fun sign() + override fun cancel() + override fun invoke(): Flow> +} + +interface WcSolanaSignAllTransactionUseCase : + WcSignUseCase, + WcSignUseCase.SimpleRun> { + + override val network: Network + override val rawSdkRequest: WcSdkSessionRequest + override val session: WcSession + + override fun sign() + override fun cancel() + override fun invoke(): Flow>> +} \ No newline at end of file