diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4f61d629c7..1634169316 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,6 +9,17 @@ plugins { id("configuration") } +//conflict of dependencies when adding WalletConnectV2.0 library +configurations { + all { + exclude(group = "org.bouncycastle", module = "bcprov-jdk15to18") + resolutionStrategy { + force("org.bouncycastle:bcpkix-jdk15on:1.70") + } + exclude(group = "com.github.komputing.kethereum") + } +} + dependencies { implementation(files("libs/walletconnect-1.5.6.aar")) implementation(project(":domain:legacy")) @@ -124,6 +135,8 @@ dependencies { implementation(deps.armadillo) implementation(deps.mviCore.watcher) implementation(deps.kotlin.serialization) + implementation(deps.walletConnectCore) + implementation(deps.walletConnectWeb3) /** Testing libraries */ testImplementation(deps.test.junit) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 682ebe6588..a9226c6eff 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,10 +34,11 @@ android:roundIcon="@mipmap/ic_launcher" android:supportsRtl="true" android:allowBackup="false" + android:fullBackupContent="false" android:networkSecurityConfig="@xml/network_security_config" android:hardwareAccelerated="true" tools:ignore="GoogleAppIndexingWarning" - tools:replace="android:allowBackup"> + tools:replace="android:allowBackup, android:fullBackupContent"> @@ -137,6 +131,12 @@ class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var walletFeatureToggles: WalletFeatureToggles + @Inject + lateinit var walletConnect2Repository: WalletConnect2Repository + + @Inject + lateinit var walletConnectSessionsRepository: WalletConnectSessionsRepository + override fun onCreate() { super.onCreate() @@ -151,6 +151,8 @@ class TapApplication : Application(), ImageLoaderFactory { networkConnectionManager = networkConnectionManager, customTokenFeatureToggles = customTokenFeatureToggles, walletFeatureToggles = walletFeatureToggles, + walletConnectRepository = walletConnect2Repository, + walletConnectSessionsRepository = walletConnectSessionsRepository, ), ), ) @@ -193,6 +195,7 @@ class TapApplication : Application(), ImageLoaderFactory { } initTopUpController() + walletConnect2Repository.init(projectId = configManager.config.walletConnectProjectId) } private fun initTopUpController() { diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index a66fd6caa0..e4520b39d6 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -93,14 +93,11 @@ class DialogManager : StoreSubscriber { ChooseNetworkDialog.create(state.dialog.session, state.dialog.networks, context) is WalletConnectDialog.ClipboardOrScanQr -> ClipboardOrScanQrDialog.create(state.dialog.clipboardUri, context) - is WalletConnectDialog.RequestTransaction -> TransactionDialog.create(state.dialog.dialogData, context) + is WalletConnectDialog.RequestTransaction -> TransactionDialog.create(state.dialog.data, context) is WalletConnectDialog.PersonalSign -> PersonalSignDialog.create(state.dialog.data, context) is WalletConnectDialog.BnbTransactionDialog -> BnbTransactionDialog.create( - data = state.dialog.data, - session = state.dialog.session, - sessionId = state.dialog.sessionId, - dAppName = state.dialog.dAppName, + preparedData = state.dialog.data, context = context, ) is WalletConnectDialog.UnsupportedNetwork -> @@ -109,6 +106,15 @@ class DialogManager : StoreSubscriber { messageRes = R.string.wallet_connect_scanner_error_unsupported_network, context = context, ) + is WalletConnectDialog.SessionProposalDialog -> { + SessionProposalDialog.create( + sessionProposal = state.dialog.sessionProposal, + networks = state.dialog.networks, + context = context, + onApprove = state.dialog.onApprove, + onReject = state.dialog.onReject, + ) + } is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context) is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context) is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(context) diff --git a/app/src/main/java/com/tangem/tap/common/FileReader.kt b/app/src/main/java/com/tangem/tap/common/FileReader.kt deleted file mode 100644 index 49e08b5bcb..0000000000 --- a/app/src/main/java/com/tangem/tap/common/FileReader.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.tap.common - -import android.content.Context -import com.tangem.tap.common.extensions.readFile -import com.tangem.tap.common.extensions.rewriteFile - -interface FileReader { - fun readFile(fileName: String): String - fun rewriteFile(content: String, fileName: String) -} - -class AndroidFileReader(private val context: Context) : FileReader { - override fun readFile(fileName: String): String { - return context.readFile(fileName) - } - - override fun rewriteFile(content: String, fileName: String) { - context.rewriteFile(content, fileName) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Context.kt b/app/src/main/java/com/tangem/tap/common/extensions/Context.kt index db544c0297..8cdf3302b7 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Context.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Context.kt @@ -1,20 +1,11 @@ package com.tangem.tap.common.extensions -import android.content.ContentResolver -import android.content.Context -import android.content.pm.PackageManager -import android.content.res.Resources -import android.net.Uri -import androidx.annotation.AnyRes -import androidx.core.content.ContextCompat - -fun Context.readFile(fileName: String): String = this.openFileInput(fileName).bufferedReader().readText() - -fun Context.rewriteFile(content: String, fileName: String) { - this.openFileOutput(fileName, Context.MODE_PRIVATE).use { - it.write(content.toByteArray(), 0, content.length) - } -} +import android.content.* +import android.content.pm.* +import android.content.res.* +import android.net.* +import androidx.annotation.* +import androidx.core.content.* fun Context.isPermissionGranted(permission: String): Boolean { return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED diff --git a/app/src/main/java/com/tangem/tap/common/redux/StateDialog.kt b/app/src/main/java/com/tangem/tap/common/redux/StateDialog.kt index ab957b81b1..5127aae02f 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/StateDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/StateDialog.kt @@ -20,6 +20,18 @@ sealed class AppDialog : StateDialog { val onOk: VoidCallback? = null, ) : AppDialog() + data class OkCancelDialogRes( + val headerId: Int, + val messageId: Int, + val okButton: DialogButton, + val cancelButton: DialogButton, + ) : AppDialog() + + data class DialogButton( + val title: Int, + val action: VoidCallback? = null, + ) + object ScanFailsDialog : AppDialog() data class AddressInfoDialog( diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 54e70bd879..50182bde55 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -1,10 +1,6 @@ package com.tangem.tap.domain -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.BlockchainSdkConfig -import com.tangem.blockchain.common.Token -import com.tangem.blockchain.common.Wallet -import com.tangem.blockchain.common.WalletManagerFactory +import com.tangem.blockchain.common.* import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.core.analytics.Analytics @@ -13,6 +9,7 @@ import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse import com.tangem.operations.attestation.Attestation +import com.tangem.tap.* import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchWithMain @@ -25,10 +22,6 @@ import com.tangem.tap.features.disclaimer.createDisclaimer import com.tangem.tap.features.disclaimer.redux.DisclaimerAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.wallet.redux.WalletAction -import com.tangem.tap.preferencesStorage -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager -import com.tangem.tap.walletStoresManager import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.CoroutineScope @@ -85,10 +78,24 @@ class TapWalletManager( store.dispatch(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed)) store.dispatch(WalletAction.Warnings.CheckIfNeeded) } - + setupWalletConnectV2(userWallet) loadData(userWallet, refresh) } + private fun setupWalletConnectV2(userWallet: UserWallet) { + val cardId = if (userWallet.cardsInWallet.size == 1) { + userWallet.cardId + } else { + null + } + scope.launch { + store.state.daggerGraphState.walletConnectInteractor?.startListening( + userWalletId = userWallet.walletId.stringValue, + cardId = cardId, + ) + } + } + suspend fun loadData(userWallet: UserWallet, refresh: Boolean = false) { walletStoresManager.fetch(userWallet, refresh) .doOnSuccess { diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt index bb1c27c80e..ff7517ff1a 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt @@ -7,8 +7,8 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechService import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.connection.NetworkConnectionManager +import com.tangem.datasource.files.AndroidFileReader import com.tangem.domain.models.scan.CardDTO -import com.tangem.tap.common.AndroidFileReader import com.tangem.tap.domain.model.builders.UserWalletIdBuilder import com.tangem.tap.domain.tokens.converters.CurrencyConverter import com.tangem.tap.domain.tokens.models.BlockchainNetwork diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt index 8ebee84b97..b67bb497de 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt @@ -4,7 +4,7 @@ import com.squareup.moshi.JsonAdapter import com.tangem.Log import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.tap.common.FileReader +import com.tangem.datasource.files.FileReader import com.tangem.tap.features.wallet.models.Currency class UserTokensStorageService(private val fileReader: FileReader) { diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt index 5dec1d0850..9e08d07d76 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt @@ -5,11 +5,13 @@ import com.google.gson.GsonBuilder import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.calculateSha256 import com.tangem.tap.common.extensions.stripZeroPlainString +import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder +import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder +import com.tangem.tap.domain.walletconnect2.domain.models.binance.tradeOrderSerializer import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData import com.tangem.tap.features.details.redux.walletconnect.TradeData import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder -import com.trustwallet.walletconnect.models.binance.tradeOrderSerializer import timber.log.Timber object BnbHelper { @@ -39,6 +41,50 @@ object BnbHelper { ) } + fun WcBinanceTradeOrder.toWCBinanceTradeOrder(): WCBinanceTradeOrder { + return WCBinanceTradeOrder( + account_number = accountNumber, + chain_id = chainId, + data = data, + memo = memo, + sequence = sequence, + source = source, + msgs = msgs.map { it.toWCBinanceTradeOrderMessage() }, + ) + } + + private fun WcBinanceTradeOrder.Message.toWCBinanceTradeOrderMessage(): WCBinanceTradeOrder.Message { + return WCBinanceTradeOrder.Message(id, orderType, price, quantity, sender, side, symbol, timeInforce) + } + + fun WcBinanceTransferOrder.toWCBinanceTransferOrder(): WCBinanceTransferOrder { + return WCBinanceTransferOrder( + account_number = accountNumber, + chain_id = chainId, + data = data, + memo = memo, + sequence = sequence, + source = source, + msgs = msgs.map { it.toWCBinanceTransferOrderMessage() }, + ) + } + + private fun WcBinanceTransferOrder.Message.toWCBinanceTransferOrderMessage(): WCBinanceTransferOrder.Message { + return WCBinanceTransferOrder.Message( + inputs.map { it.toWCBinanceItem() }, + outputs.map { it.toWCBinanceItem() }, + ) + } + + private fun WcBinanceTransferOrder.Message.Item.toWCBinanceItem(): WCBinanceTransferOrder.Message.Item { + return WCBinanceTransferOrder.Message.Item( + address, + coins.map { + WCBinanceTransferOrder.Message.Item.Coin(it.amount, it.denom) + }, + ) + } + fun createMessageData(order: WCBinanceTradeOrder): BinanceMessageData.Trade { val address = order.msgs.first().sender diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt index 64037f50e2..7b80ce1039 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt @@ -4,19 +4,18 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics +import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError import com.tangem.tap.domain.walletconnect.extensions.isDappSupported -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession -import com.tangem.tap.features.details.redux.walletconnect.WalletForSession -import com.tangem.tap.features.details.redux.walletconnect.WcPersonalSignData -import com.tangem.tap.features.details.redux.walletconnect.WcTransactionData -import com.tangem.tap.features.details.redux.walletconnect.WcTransactionType +import com.tangem.tap.domain.walletconnect.extensions.toWcEthereumSignMessage +import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction +import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest +import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData +import com.tangem.tap.features.details.redux.walletconnect.* import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.walletConnectRepository @@ -61,7 +60,7 @@ class WalletConnectManager { HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } } - private var sessions: MutableMap = mutableMapOf() + private var sessions: MutableMap = mutableMapOf() fun connect(wcUri: String) { val session = WCSession.from(wcUri).guard { @@ -73,7 +72,7 @@ class WalletConnectManager { ) return } - if (sessions[session] != null) { + if (sessions[session.topic] != null) { store.dispatchOnMain(WalletConnectAction.RefuseOpeningSession) return } @@ -93,7 +92,7 @@ class WalletConnectManager { return } - sessions[session] = WalletConnectActiveData( + sessions[session.topic] = WalletConnectActiveData( peerId = peerId, remotePeerId = null, session = session, @@ -104,26 +103,26 @@ class WalletConnectManager { } fun updateSession(session: WalletConnectSession) { - val updatedSession = sessions[session.session]?.copy( + val updatedSession = sessions[session.session.topic]?.copy( wallet = session.wallet, ) if (updatedSession != null) { - sessions[session.session] = updatedSession + sessions[session.session.topic] = updatedSession } } fun updateBlockchain(session: WalletConnectSession) { - sessions[session.session]?.client?.updateSession( + sessions[session.session.topic]?.client?.updateSession( accounts = listOfNotNull(session.getAddress()), chainId = session.wallet.blockchain?.getChainId(), approved = true, ) - val updatedSession = sessions[session.session]?.copy( + val updatedSession = sessions[session.session.topic]?.copy( wallet = session.wallet, ) if (updatedSession != null) { - sessions[session.session] = updatedSession + sessions[session.session.topic] = updatedSession } } @@ -131,7 +130,7 @@ class WalletConnectManager { private fun setupConnectionTimeoutCheck(session: WCSession) { scope.launch { delay(20_000) - val data = sessions[session] + val data = sessions[session.topic] if (data != null && data.peerMeta == null) { disconnect(session) store.dispatchOnMain(WalletConnectAction.OpeningSessionTimeout(session)) @@ -160,13 +159,13 @@ class WalletConnectManager { setListeners(it.client) it.client.connect(it.session, tangemPeerMeta, it.peerId, it.remotePeerId) } - }.associateBy { it.session }.toMutableMap() + }.associateBy { it.session.topic }.toMutableMap() store.dispatchOnMain(WalletConnectAction.SetSessionsRestored(sessions)) } fun approve(session: WCSession) { - val activeData = sessions[session] ?: return + val activeData = sessions[session.topic] ?: return removeSimilarSessions(activeData) val key = activeData.wallet.derivedPublicKey ?: activeData.wallet.walletPublicKey ?: return @@ -203,18 +202,18 @@ class WalletConnectManager { sessionsToRemove.forEach { disconnect(it.value.session) } } - fun rejectRequest(session: WCSession, id: Long) { - val activeData = sessions[session] ?: return + fun rejectRequest(topic: String, id: Long) { + val activeData = sessions[topic] ?: return activeData.client.rejectRequest(id) } - fun acceptRequest(session: WCSession, id: Long, data: String) { - val activeData = sessions[session] ?: return + fun acceptRequest(topic: String, id: Long, data: String) { + val activeData = sessions[topic] ?: return activeData.client.approveRequest(id, data) } fun disconnect(session: WCSession) { - val activeData = sessions[session] ?: return + val activeData = sessions[session.topic] ?: return val disconnected = if (activeData.client.isConnected) { activeData.client.killSession() } else { @@ -228,118 +227,145 @@ class WalletConnectManager { private fun onSessionClosed(session: WCSession) { Analytics.send(WalletConnect.SessionDisconnected()) - sessions.remove(session) + sessions.remove(session.topic) walletConnectRepository.removeSession(session) store.dispatchOnMain(WalletConnectAction.RemoveSession(session)) } fun handleTransactionRequest( - transaction: WCEthereumTransaction, + transaction: WcEthereumTransaction, session: WalletConnectSession, id: Long, - type: WcTransactionType, + type: WcEthTransactionType, ) { - val activeData = sessions[session.session] ?: return + val activeData = sessions[session.session.topic] ?: return scope.launch { val data = WalletConnectSdkHelper().prepareTransactionData( - transaction = transaction, - session = session, - id = id, - type = type, + EthTransactionData( + transaction = transaction, + networkId = session.wallet.blockchain?.toNetworkId() ?: "", + rawDerivationPath = session.wallet.derivationPath?.rawPath, + id = id, + topic = session.session.topic, + type = type, + metaName = session.peerMeta.name, + metaUrl = session.peerMeta.url, + ), ).guard { - sessions[session.session] = activeData.copy(transactionData = null) + sessions[session.session.topic] = activeData.copy(transactionData = null) store.dispatchOnMain( WalletConnectAction.RejectRequest( - session.session, + session.session.topic, id, ), ) return@launch } - sessions[session.session] = activeData.copy(transactionData = data) + sessions[session.session.topic] = activeData.copy(transactionData = data) store.dispatchOnMain( GlobalAction.ShowDialog( WalletConnectDialog.RequestTransaction( - data.dialogData, + WcPreparedRequest.EthTransaction( + preparedRequestData = data, + topic = session.session.topic, + requestId = id, + derivationPath = data.walletManager.wallet.publicKey.derivationPath?.rawPath, + ), ), ), ) } } - fun completeTransaction(session: WCSession) { - val activeData = sessions[session] + fun completeTransaction(topic: Topic) { + val activeData = sessions[topic] val data = activeData?.transactionData ?: return scope.launch { val hash = WalletConnectSdkHelper().completeTransaction(data, cardId).guard { - sessions[data.session.session] = activeData.copy(transactionData = null) + sessions[topic] = activeData.copy(transactionData = null) store.dispatchOnMain( WalletConnectAction.RejectRequest( - data.session.session, + topic, data.id, ), ) return@launch } - acceptRequest(session, data.id, hash) - sessions[session] = activeData.copy(transactionData = null) + acceptRequest(topic, data.id, hash) + sessions[topic] = activeData.copy(transactionData = null) } } - fun signBnb(id: Long, data: ByteArray, sessionData: WCSession) { - val activeData = sessions[sessionData] ?: return + fun signBnb(id: Long, data: ByteArray, topic: Topic) { + val activeData = sessions[topic] ?: return scope.launch { - val hash = WalletConnectSdkHelper().signBnbTransaction(data, activeData, cardId).guard { + val hash = WalletConnectSdkHelper().signBnbTransaction( + data = data, + networkId = activeData.wallet.blockchain?.toNetworkId() ?: "", + derivationPath = activeData.wallet.derivationPath?.rawPath, + cardId = cardId, + ).guard { store.dispatchOnMain( WalletConnectAction.RejectRequest( - sessionData, + topic, id, ), ) return@launch } - acceptRequest(sessionData, id, hash) + acceptRequest(topic, id, hash) } } fun handlePersonalSignRequest(message: WCEthereumSignMessage, session: WalletConnectSession, id: Long) { - val activeData = sessions[session.session] ?: return + val activeData = sessions[session.session.topic] ?: return scope.launch { val data = WalletConnectSdkHelper().prepareDataForPersonalSign( - message = message, - session = session, + message = message.toWcEthereumSignMessage(), + topic = session.session.topic, id = id, + metaName = session.peerMeta.name, ) - sessions[session.session] = activeData.copy(personalSignData = data) + sessions[session.session.topic] = activeData.copy(personalSignData = data) store.dispatchOnMain( GlobalAction.ShowDialog( WalletConnectDialog.PersonalSign( - data.dialogData, + WcPreparedRequest.EthSign( + preparedRequestData = data, + topic = session.session.topic, + requestId = id, + derivationPath = session.wallet.derivationPath?.rawPath, + ), ), ), ) } } - fun sendSignedMessage(session: WCSession) { - val activeData = sessions[session] + fun sendSignedMessage(topic: Topic) { + val activeData = sessions[topic] val data = activeData?.personalSignData ?: return scope.launch { - val hash = WalletConnectSdkHelper().signPersonalMessage(data.hash, activeData.wallet, cardId) + val hash = WalletConnectSdkHelper().signPersonalMessage( + hashToSign = data.hash, + networkId = activeData.wallet.blockchain?.toNetworkId() ?: "", + derivationPath = activeData.wallet.derivationPath?.rawPath, + cardId = cardId, + ) .guard { - sessions[data.session.session] = activeData.copy(transactionData = null) + sessions[topic] = activeData.copy(transactionData = null) store.dispatchOnMain( WalletConnectAction.RejectRequest( - data.session.session, + topic, data.id, ), ) return@launch } - sessions[session] = activeData.copy(personalSignData = data) - acceptRequest(data.session.session, data.id, hash) - sessions[data.session.session] = activeData.copy(transactionData = null) + sessions[topic] = activeData.copy(personalSignData = data) + acceptRequest(topic, data.id, hash) + sessions[topic] = activeData.copy(transactionData = null) } } @@ -348,7 +374,7 @@ class WalletConnectManager { client.onSessionRequest = { id: Long, peer: WCPeerMeta -> Timber.d("OnSessionRequest: $peer") val session = client.session - val data = sessions[session]?.copy(peerMeta = peer, remotePeerId = client.remotePeerId) + val data = sessions[session?.topic]?.copy(peerMeta = peer, remotePeerId = client.remotePeerId) if (data != null && session != null) { if (!peer.isDappSupported()) { store.dispatchOnMain( @@ -358,7 +384,7 @@ class WalletConnectManager { ), ) } else { - sessions[session] = data + sessions[session.topic] = data val sessionData = data.toWalletConnectSession() sessionData?.let { store.dispatchOnMain( @@ -384,13 +410,13 @@ class WalletConnectManager { // AnalyticsAnOld.WcAction.SendTransaction // ) // ) - sessions[client.session]?.toWalletConnectSession()?.let { sessionData -> + sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData -> store.dispatchOnMain( WalletConnectAction.HandleTransactionRequest( transaction = transaction, session = sessionData, id = id, - type = WcTransactionType.EthSendTransaction, + type = WcEthTransactionType.EthSendTransaction, ), ) } @@ -402,13 +428,13 @@ class WalletConnectManager { // AnalyticsAnOld.WcAction.SignTransaction // ) // ) - sessions[client.session]?.toWalletConnectSession()?.let { sessionData -> + sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData -> store.dispatchOnMain( WalletConnectAction.HandleTransactionRequest( transaction = transaction, session = sessionData, id = id, - type = WcTransactionType.EthSignTransaction, + type = WcEthTransactionType.EthSignTransaction, ), ) } @@ -420,7 +446,7 @@ class WalletConnectManager { // AnalyticsAnOld.WcAction.PersonalSign // ) // ) - sessions[client.session]?.toWalletConnectSession()?.let { sessionData -> + sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData -> store.dispatchOnMain( WalletConnectAction.HandlePersonalSignRequest( message, @@ -434,7 +460,7 @@ class WalletConnectManager { client.onBnbCancel = { id: Long, order: WCBinanceCancelOrder -> } client.onBnbTrade = { id: Long, order: WCBinanceTradeOrder -> - sessions[client.session]?.toWalletConnectSession()?.let { sessionData -> + sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData -> store.dispatchOnMain( WalletConnectAction.BinanceTransaction.Trade( id = id, @@ -445,7 +471,7 @@ class WalletConnectManager { } } client.onBnbTransfer = { id: Long, order: WCBinanceTransferOrder -> - sessions[client.session]?.toWalletConnectSession()?.let { sessionData -> + sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData -> store.dispatchOnMain( WalletConnectAction.BinanceTransaction.Transfer( id = id, @@ -484,7 +510,7 @@ class WalletConnectManager { private fun switchChain(chainId: Int, client: WCClient) { val blockchain = Blockchain.fromChainId(chainId) Timber.d("WC switch chainID\nNew Blockchain: $blockchain") - val session = sessions[client.session]?.toWalletConnectSession() + val session = sessions[client.session?.topic]?.toWalletConnectSession() if (session != null) { store.dispatchOnMain(WalletConnectAction.SwitchBlockchain(blockchain, session)) } @@ -499,6 +525,8 @@ class WalletConnectManager { } } +typealias Topic = String + data class WalletConnectActiveData( val peerId: String, val remotePeerId: String?, diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index 5ccf53357d..57afa93913 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -5,14 +5,7 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumGasLoader import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumUtils import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.Companion.toKeccak -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.BlockchainSdkError -import com.tangem.blockchain.common.CommonSigner -import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.TransactionSender -import com.tangem.blockchain.common.Wallet -import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.* import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.extensions.hexToBigDecimal @@ -23,41 +16,40 @@ import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toDecompressedPublicKey import com.tangem.common.extensions.toHexString import com.tangem.core.analytics.Analytics -import com.tangem.crypto.CryptoUtils +import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.operations.sign.SignHashCommand import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.extensions.toFormattedString -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession -import com.tangem.tap.features.details.redux.walletconnect.WalletForSession +import com.tangem.tap.domain.tokens.models.BlockchainNetwork +import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTradeOrder +import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTransferOrder +import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage +import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction +import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData +import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder +import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder +import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData +import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType import com.tangem.tap.features.details.redux.walletconnect.WcPersonalSignData import com.tangem.tap.features.details.redux.walletconnect.WcTransactionData -import com.tangem.tap.features.details.redux.walletconnect.WcTransactionType import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData import com.tangem.tap.store import com.tangem.tap.tangemSdk import com.tangem.tap.tangemSdkManager -import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage -import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage.WCSignType.MESSAGE -import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage.WCSignType.PERSONAL_MESSAGE -import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage.WCSignType.TYPED_MESSAGE -import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction import timber.log.Timber import java.math.BigDecimal class WalletConnectSdkHelper { @Suppress("MagicNumber") - suspend fun prepareTransactionData( - transaction: WCEthereumTransaction, - session: WalletConnectSession, - id: Long, - type: WcTransactionType, - ): WcTransactionData? { - val walletManager = getWalletManager(session) ?: return null + suspend fun prepareTransactionData(data: EthTransactionData): WcTransactionData? { + val transaction = data.transaction + val blockchain = Blockchain.fromNetworkId(data.networkId) ?: return null + val walletManager = getWalletManager(blockchain, data.rawDerivationPath) ?: return null walletManager.safeUpdate() val wallet = walletManager.wallet @@ -94,53 +86,49 @@ class WalletConnectSdkHelper { nonce = transaction.nonce?.hexToBigDecimal()?.toBigInteger(), ), ) + val dialogData = TransactionRequestDialogData( - dAppName = session.peerMeta.name, - dAppUrl = session.peerMeta.url, + dAppName = data.metaName, + dAppUrl = data.metaUrl, amount = value.toFormattedString(decimals), gasAmount = fee.toFormattedString(decimals), totalAmount = total.toFormattedString(decimals), balance = balance.toFormattedString(decimals), isEnoughFundsToSend = balance - total >= BigDecimal.ZERO, - session = session.session, - id = id, - type = type, + topic = data.topic, + id = data.id, + type = data.type, ) return WcTransactionData( - type = type, + type = data.type, transaction = transactionData, - session = session, - id = id, + topic = data.topic, + id = data.id, walletManager = walletManager, dialogData = dialogData, ) } - private fun getWalletManager(session: WalletConnectSession): WalletManager? { - val factory = store.state.globalState.tapWalletManager.walletManagerFactory - val publicKey = Wallet.PublicKey( - session.wallet.walletPublicKey ?: return null, - session.wallet.derivedPublicKey, - session.wallet.derivationPath, - ) - val blockchain = session.wallet.getBlockchainForSession() - return factory.makeWalletManager( + private fun getWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager? { + val blockchainNetwork = BlockchainNetwork( blockchain = blockchain, - publicKey = publicKey, + derivationPath = derivationPath, + tokens = emptyList(), ) + return store.state.walletState.getWalletManager(blockchainNetwork) } suspend fun completeTransaction(data: WcTransactionData, cardId: String?): String? { return when (data.type) { - WcTransactionType.EthSendTransaction -> sendTransaction(data, cardId) - WcTransactionType.EthSignTransaction -> signTransaction(data, cardId) + WcEthTransactionType.EthSendTransaction -> sendTransaction(data, cardId) + WcEthTransactionType.EthSignTransaction -> signTransaction(data, cardId) } } private suspend fun getGasLimitFromTx( value: BigDecimal, walletManager: WalletManager, - transaction: WCEthereumTransaction, + transaction: WcEthereumTransaction, ): BigDecimal { return transaction.gas?.hexToBigDecimal() ?: transaction.gasLimit?.hexToBigDecimal() @@ -154,7 +142,7 @@ class WalletConnectSdkHelper { private suspend fun getGaLimitFromBlockchain( value: BigDecimal, walletManager: WalletManager, - transaction: WCEthereumTransaction, + transaction: WcEthereumTransaction, ): BigDecimal { val gasLimitResult = (walletManager as? EthereumGasLoader)?.getGasLimit( amount = Amount(value, walletManager.wallet.blockchain), @@ -216,17 +204,32 @@ class WalletConnectSdkHelper { } } - suspend fun signBnbTransaction(data: ByteArray, session: WalletConnectActiveData, cardId: String?): String? { + fun prepareBnbTradeOrder(data: WcBinanceTradeOrder): BinanceMessageData.Trade { + return BnbHelper.createMessageData(data.toWCBinanceTradeOrder()) + } + + fun prepareBnbTransferOrder(data: WcBinanceTransferOrder): BinanceMessageData.Transfer { + return BnbHelper.createMessageData(data.toWCBinanceTransferOrder()) + } + + suspend fun signBnbTransaction( + data: ByteArray, + networkId: String, + derivationPath: String?, + cardId: String?, + ): String? { + val blockchain = Blockchain.fromNetworkId(networkId) ?: return null + val wallet = getWalletManager(blockchain, derivationPath)?.wallet ?: return null + val command = SignHashCommand( hash = data, - walletPublicKey = session.wallet.walletPublicKey ?: return null, - derivationPath = session.wallet.derivationPath, + walletPublicKey = wallet.publicKey.seedKey, + derivationPath = wallet.publicKey.derivationPath, ) val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId) return when (result) { is CompletionResult.Success -> { - val key = session.wallet.derivedPublicKey?.toDecompressedPublicKey() - ?: session.wallet.walletPublicKey.toDecompressedPublicKey() + val key = wallet.publicKey.blockchainKey.toDecompressedPublicKey() getBnbResultString( key.toHexString(), result.data.signature.toHexString(), @@ -241,13 +244,16 @@ class WalletConnectSdkHelper { } fun prepareDataForPersonalSign( - message: WCEthereumSignMessage, - session: WalletConnectSession, + message: WcEthereumSignMessage, + topic: String, + metaName: String, id: Long, ): WcPersonalSignData { val messageData = when (message.type) { - MESSAGE, PERSONAL_MESSAGE -> createMessageData(message) - TYPED_MESSAGE -> EthereumUtils.makeTypedDataHash(message.data) + WcEthereumSignMessage.WCSignType.MESSAGE, + WcEthereumSignMessage.WCSignType.PERSONAL_MESSAGE, + -> createMessageData(message) + WcEthereumSignMessage.WCSignType.TYPED_MESSAGE -> EthereumUtils.makeTypedDataHash(message.data) } val messageString = message.data.hexToAscii() @@ -255,20 +261,21 @@ class WalletConnectSdkHelper { ?: message.data val dialogData = PersonalSignDialogData( - dAppName = session.peerMeta.name, + dAppName = metaName, message = messageString, - session = session.session, + topic = topic, id = id, ) return WcPersonalSignData( hash = messageData, - session = session, + topic = topic, id = id, dialogData = dialogData, + type = message.type, ) } - private fun createMessageData(message: WCEthereumSignMessage): ByteArray { + private fun createMessageData(message: WcEthereumSignMessage): ByteArray { val messageData = try { message.data.removePrefix(HEX_PREFIX).hexToBytes() } catch (exception: Exception) { @@ -299,16 +306,27 @@ class WalletConnectSdkHelper { }.joinToString("") } - suspend fun signPersonalMessage(hashToSign: ByteArray, wallet: WalletForSession, cardId: String?): String? { - val key = wallet.derivedPublicKey ?: wallet.walletPublicKey - val command = SignHashCommand(hashToSign, wallet.walletPublicKey!!, wallet.derivationPath) + suspend fun signPersonalMessage( + hashToSign: ByteArray, + networkId: String, + derivationPath: String?, + cardId: String?, + ): String? { + val blockchain = Blockchain.fromNetworkId(networkId) ?: return null + val wallet = getWalletManager(blockchain, derivationPath)?.wallet ?: return null + + val command = SignHashCommand( + hash = hashToSign, + walletPublicKey = wallet.publicKey.seedKey, + derivationPath = wallet.publicKey.derivationPath, + ) return when (val result = tangemSdkManager.runTaskAsync(command, cardId)) { is CompletionResult.Success -> { val hash = result.data.signature return EthereumUtils.prepareSignedMessageData( signedHash = hash, hashToSign = hashToSign, - publicKey = CryptoUtils.decompressPublicKey(key!!), + publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(), ) } is CompletionResult.Failure -> { diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt index 057caf7bd2..3197f2729c 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt @@ -1,9 +1,39 @@ package com.tangem.tap.domain.walletconnect.extensions +import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage +import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction import com.trustwallet.walletconnect.models.WCPeerMeta +import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage +import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction fun WCPeerMeta.isDappSupported(): Boolean { return !unsupportedDappsList.any { this.url.contains(it) } } -private val unsupportedDappsList: List = listOf("dydx.exchange") \ No newline at end of file +private val unsupportedDappsList: List = listOf("dydx.exchange") + +fun WCEthereumTransaction.toWcEthTransaction(): WcEthereumTransaction { + return WcEthereumTransaction( + from = from, + to = to, + nonce = nonce, + gasPrice = gasPrice, + maxFeePerGas = maxFeePerGas, + maxPriorityFeePerGas = maxPriorityFeePerGas, + gas = gas, + gasLimit = gasLimit, + value = value, + data = data, + ) +} + +fun WCEthereumSignMessage.toWcEthereumSignMessage(): WcEthereumSignMessage { + return WcEthereumSignMessage( + raw = raw, + type = when (type) { + WCEthereumSignMessage.WCSignType.MESSAGE -> WcEthereumSignMessage.WCSignType.MESSAGE + WCEthereumSignMessage.WCSignType.PERSONAL_MESSAGE -> WcEthereumSignMessage.WCSignType.PERSONAL_MESSAGE + WCEthereumSignMessage.WCSignType.TYPED_MESSAGE -> WcEthereumSignMessage.WCSignType.TYPED_MESSAGE + }, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt new file mode 100644 index 0000000000..6a584a0548 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt @@ -0,0 +1,63 @@ +package com.tangem.tap.domain.walletconnect2.app + +import com.tangem.blockchain.common.* +import com.tangem.domain.common.extensions.* +import com.tangem.tap.domain.walletconnect2.domain.* + +class TangemWcBlockchainHelper : WcBlockchainHelper { + override fun chainIdToNetworkIdOrNull(chainId: String): String? { + val parsed = chainId.split(CHAIN_SEPARATOR) + if (parsed.size != 2) return null + when { + parsed.first() == EVM_NAMESPACE -> { + val chainIdInt = parsed[1].toIntOrNull() ?: return null + return Blockchain.fromChainId(chainIdInt)?.toNetworkId() + } + else -> { + val blockchain = Blockchain.fromNetworkId(parsed.first()) + return if (supportedNonEvmBlockchains.contains(blockchain)) blockchain?.toNetworkId() else null + } + } + } + + override fun networkIdToChainIdOrNull(networkId: String): String? { + val blockchain = Blockchain.fromNetworkId(networkId) + val namespace = blockchain?.getCaip2Namespace() ?: return null + val chainId = blockchain.getCaip2ChainId() ?: return null + return "$namespace$CHAIN_SEPARATOR$chainId" + } + + override fun getNamespaceFromFullChainIdOrNull(chainId: String): String? { + val parsed = chainId.split(CHAIN_SEPARATOR) + return parsed.firstOrNull() + } + + override fun chainIdToFullNameOrNull(chainId: String): String? { + val networkId = chainIdToNetworkIdOrNull(chainId) ?: return null + return Blockchain.fromNetworkId(networkId)?.fullName + } +} + +private fun Blockchain.getCaip2ChainId(): String? { + if (this.isEvm()) return this.getChainId()?.toString() + + return when (this) { + Blockchain.Solana -> "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ" + Blockchain.Polkadot -> "91b171bb158e2d3848fa23a9f1c25182" + Blockchain.Tron -> "0x2b6653dc" + else -> null + } +} + +private fun Blockchain.getCaip2Namespace(): String? { + return when { + this.isEvm() -> EVM_NAMESPACE + supportedNonEvmBlockchains.contains(this) -> this.toNetworkId() + else -> null + } +} + +private val supportedNonEvmBlockchains = emptySet() // TODO: add supported networks + +private const val EVM_NAMESPACE = "eip155" +private const val CHAIN_SEPARATOR = ":" \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt new file mode 100644 index 0000000000..d6767ddc8e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt @@ -0,0 +1,49 @@ +package com.tangem.tap.domain.walletconnect2.app + +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectEventsHandler +import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest +import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError +import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog +import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen +import com.tangem.tap.store +import timber.log.Timber + +class WalletConnectEventsHandlerImpl : WalletConnectEventsHandler { + override fun onProposalReceived(proposal: WalletConnectEvents.SessionProposal, networksFormatted: String) { + store.dispatchOnMain( + GlobalAction.ShowDialog( + WalletConnectDialog.SessionProposalDialog( + sessionProposal = proposal, + networks = networksFormatted, + onApprove = { store.dispatchOnMain(WalletConnectAction.ApproveProposal) }, + onReject = { store.dispatchOnMain(WalletConnectAction.RejectProposal) }, + ), + ), + ) + } + + override fun onSessionEstablished() { + store.dispatchOnMain(WalletConnectAction.SessionEstablished) + } + + override fun onSessionRejected(error: WalletConnectError) { + store.dispatchOnMain(WalletConnectAction.SessionRejected(error)) + } + + override fun onListOfSessionsUpdated(sessions: List) { + Timber.d("WC2: List of sessions updated. Sessions: $sessions") + store.dispatchOnMain(WalletConnectAction.SessionListUpdated(sessions)) + } + + override fun onSessionRequest(request: WcPreparedRequest) { + store.dispatchOnMain(WalletConnectAction.ShowSessionRequest(request)) + } + + override fun onUnsupportedRequest() { + store.dispatchOnMain(WalletConnectAction.RejectUnsupportedRequest) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt new file mode 100644 index 0000000000..80d7ed0b09 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt @@ -0,0 +1,327 @@ +package com.tangem.tap.domain.walletconnect2.data + +import android.app.* +import com.tangem.tap.domain.walletconnect2.domain.* +import com.tangem.tap.domain.walletconnect2.domain.models.* +import com.walletconnect.android.* +import com.walletconnect.android.relay.* +import com.walletconnect.web3.wallet.client.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import timber.log.* +import javax.inject.* + +class WalletConnectRepositoryImpl @Inject constructor( + private val application: Application, + private val wcRequestDeserializer: WcJrpcRequestsDeserializer, +) : WalletConnectRepository { + + private var sessionProposal: Wallet.Model.SessionProposal? = null + private var userNamespaces: Map>? = null + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + private val _events: MutableSharedFlow = MutableSharedFlow() + override val events: Flow = _events + + private val _activeSessions: MutableSharedFlow> = MutableSharedFlow() + override val activeSessions: Flow> = _activeSessions + + /** + * @param projectId Project ID at https://cloud.walletconnect.com/ + */ + override fun init(projectId: String) { + val relayUrl = "relay.walletconnect.com" + val serverUrl = "wss://$relayUrl?projectId=$projectId" + val connectionType = ConnectionType.AUTOMATIC + + val appMetaData = Core.Model.AppMetaData( + name = "Tangem", + description = "Tangem Wallet", + url = "tangem.com", + icons = listOf( + "https://user-images.githubusercontent.com/24321494/124071202-72a00900-da58-11eb-935a-dcdab21de52b.png", + ), + redirect = "kotlin-wallet-wc:/request", // Custom Redirect URI + ) + + CoreClient.initialize( + relayServerUrl = serverUrl, + connectionType = connectionType, + application = application, + metaData = appMetaData, + ) { error -> + Timber.d("Error while initializing client: $error") + scope.launch { + _events.emit( + WalletConnectEvents.SessionApprovalError( + WalletConnectError.ExternalApprovalError(error.throwable.message), + ), + ) + } + } + + Web3Wallet.initialize(Wallet.Params.Init(core = CoreClient)) { error -> + Timber.d("Error while initializing Web3Wallet: $error") + scope.launch { + _events.emit( + WalletConnectEvents.SessionApprovalError( + WalletConnectError.ExternalApprovalError(error.throwable.message), + ), + ) + } + } + + val walletDelegate = defineWalletDelegate() + Web3Wallet.setWalletDelegate(walletDelegate) + } + + private fun defineWalletDelegate(): Web3Wallet.WalletDelegate { + return object : Web3Wallet.WalletDelegate { + override fun onSessionProposal(sessionProposal: Wallet.Model.SessionProposal) { + // Triggered when wallet receives the session proposal sent by a Dapp + Timber.d("sessionProposal: $sessionProposal") + this@WalletConnectRepositoryImpl.sessionProposal = sessionProposal + + scope.launch { + _events.emit( + WalletConnectEvents.SessionProposal( + sessionProposal.name, + sessionProposal.description, + sessionProposal.url, + sessionProposal.icons, + sessionProposal.requiredNamespaces.values.flatMap { it.chains ?: emptyList() }, + ), + ) + } + } + + override fun onSessionRequest(sessionRequest: Wallet.Model.SessionRequest) { + // Triggered when a Dapp sends SessionRequest to sign a transaction or a message + Timber.d("sessionRequest: $sessionRequest") + val request = wcRequestDeserializer.deserialize( + method = sessionRequest.request.method, + params = sessionRequest.request.params, + ) + Timber.d("sessionRequestParsed: $request") + + scope.launch { + _events.emit( + WalletConnectEvents.SessionRequest( + request = request, + chainId = sessionRequest.chainId, + topic = sessionRequest.topic, + id = sessionRequest.request.id, + metaUrl = sessionRequest.peerMetaData?.url ?: "", + metaName = sessionRequest.peerMetaData?.name ?: "", + ), + ) + } + } + + override fun onAuthRequest(authRequest: Wallet.Model.AuthRequest) { + // Triggered when Dapp / Requester makes an authorization request + } + + override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) { + // Triggered when the session is deleted by the peer + if (sessionDelete is Wallet.Model.SessionDelete.Success) { + scope.launch { + _events.emit(WalletConnectEvents.SessionDeleted(sessionDelete.topic)) + } + updateSessions() + } + Timber.d("onSessionDelete: $sessionDelete") + } + + override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) { + // Triggered when wallet receives the session settlement response from Dapp + Timber.d("onSessionSettleResponse: $settleSessionResponse") + if (settleSessionResponse is Wallet.Model.SettledSessionResponse.Result) { + scope.launch { + _events.emit( + WalletConnectEvents.SessionApprovalSuccess( + topic = settleSessionResponse.session.topic, + accounts = userNamespaces?.flatMap { it.value } ?: emptyList(), + ), + ) + } + updateSessions() + } + } + + override fun onSessionUpdateResponse(sessionUpdateResponse: Wallet.Model.SessionUpdateResponse) { + // Triggered when wallet receives the session update response from Dapp + Timber.d("onSessionUpdateResponse: $sessionUpdateResponse") + updateSessions() + } + + override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) { + // Triggered whenever the connection state is changed + Timber.d("onConnectionStateChange: $state") + if (state.isAvailable) updateSessions() + } + + override fun onError(error: Wallet.Model.Error) { + // Triggered whenever there is an issue inside the SDK + } + } + } + + override fun pair(uri: String) { + Web3Wallet.pair(Wallet.Params.Pair(uri)) + } + + override fun approve(userNamespaces: Map>) { + this.userNamespaces = userNamespaces + + val sessionProposal: Wallet.Model.SessionProposal = requireNotNull(this.sessionProposal) + + val missingNetworks = findMissingNetworks( + namespaces = sessionProposal.requiredNamespaces, + userNamespaces = userNamespaces, + ) + if (missingNetworks.isNotEmpty()) { + Timber.e("Unsupported blockchains: $missingNetworks") + scope.launch { + _events.emit( + WalletConnectEvents.SessionApprovalError( + WalletConnectError.ApprovalErrorMissingNetworks(missingNetworks.toList()), + ), + ) + } + return + } + + val userChains = userNamespaces.flatMap { namespace -> + namespace.value.map { it.chainId to "${it.chainId}:${it.walletAddress}" } + }.groupBy { pair -> pair.first } + .mapValues { entry -> entry.value.map { pair -> pair.second }.toSet() } + + val preparedNamespaces = sessionProposal.requiredNamespaces.map { requiredNamespace -> + val accounts = requiredNamespace.value.chains?.mapNotNull { userChains[it] }?.flatten() ?: emptyList() + + requiredNamespace.key to Wallet.Model.Namespace.Session( + accounts = accounts, + methods = requiredNamespace.value.methods, + events = requiredNamespace.value.events, + ) + }.toMap() + + val sessionApproval = Wallet.Params.SessionApprove(sessionProposal.proposerPublicKey, preparedNamespaces) + + Timber.d("Session approval is prepared for sending: $sessionApproval") + + Web3Wallet.approveSession( + params = sessionApproval, + onSuccess = { + Timber.d("Approved successfully: $it") + }, + onError = { + Timber.d("Error while approving: $it") + scope.launch { + _events.emit( + WalletConnectEvents.SessionApprovalError( + WalletConnectError.ExternalApprovalError(it.throwable.message), + ), + ) + } + }, + ) + } + + override fun sendRequest(topic: String, id: Long, result: String) { + Web3Wallet.respondSessionRequest( + params = Wallet.Params.SessionRequestResponse( + sessionTopic = topic, + jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcResult( + id = id, + result = result, + ), + ), + onSuccess = {}, + onError = {}, + ) + } + + override fun rejectRequest(topic: String, id: Long) { + Web3Wallet.respondSessionRequest( + params = Wallet.Params.SessionRequestResponse( + sessionTopic = topic, + jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcError( + id = id, + code = 0, + message = "", + ), + ), + onSuccess = {}, + onError = {}, + ) + } + + override fun reject() { + Web3Wallet.rejectSession( + params = Wallet.Params.SessionReject( + sessionProposal?.proposerPublicKey ?: "", + "", + ), + onSuccess = { + Timber.d("Rejected successfully: $it") + }, + onError = { + Timber.d("Error while rejecting: $it") + }, + ) + } + + override fun disconnect(topic: String) { + Web3Wallet.disconnectSession( + params = Wallet.Params.SessionDisconnect(topic), + onSuccess = { + updateSessions() + Timber.d("Disconnected successfully: $it") + }, + onError = { + Timber.d("Error while disconnecting: $it") + }, + ) + } + + fun send(topic: String, id: Long, data: String) { + Web3Wallet.respondSessionRequest( + params = Wallet.Params.SessionRequestResponse( + sessionTopic = topic, + jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcResult( + id = id, + result = data, + ), + ), + onError = {}, + onSuccess = {}, + ) + } + + override fun updateSessions() { + scope.launch { + val availableSessions = Web3Wallet.getListOfActiveSessions() + .map { + WalletConnectSession( + topic = it.topic, + icon = it.metaData?.icons?.firstOrNull(), + name = it.metaData?.name, + url = it.metaData?.url, + ) + } + Timber.d("Available sessions: $availableSessions") + _activeSessions.emit(availableSessions) + } + } + + private fun findMissingNetworks( + namespaces: Map, + userNamespaces: Map>, + ): Collection { + val requiredChains = namespaces.values.flatMap { it.chains ?: emptyList() } + val userChains = userNamespaces.flatMap { it.value.map { account -> account.chainId } } + return requiredChains.subtract(userChains.toSet()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectSessionsRepositoryImpl.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectSessionsRepositoryImpl.kt new file mode 100644 index 0000000000..fe3aab634c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectSessionsRepositoryImpl.kt @@ -0,0 +1,59 @@ +package com.tangem.tap.domain.walletconnect2.data + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import com.tangem.datasource.di.SdkMoshi +import com.tangem.datasource.files.FileReader +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository +import com.tangem.tap.domain.walletconnect2.domain.models.Session +import timber.log.Timber +import javax.inject.Inject + +class WalletConnectSessionsRepositoryImpl @Inject constructor( + @SdkMoshi private val moshi: Moshi, + private val fileReader: FileReader, +) : + WalletConnectSessionsRepository { + + private val sessionsAdapter: JsonAdapter> = moshi.adapter( + Types.newParameterizedType(List::class.java, Session::class.java), + ) + + override suspend fun loadSessions(userWallet: String): List { + return try { + val fileContent = fileReader.readFile(getFileNameForUserWallet(userWallet)) + sessionsAdapter.fromJson(fileContent) ?: emptyList() + } catch (exception: Exception) { + Timber.e(exception) + emptyList() + } + } + + override suspend fun saveSession(userWallet: String, session: Session) { + val updatedList = loadSessions(userWallet).plus(session) + writeSessionToFile(sessions = updatedList, userWallet = userWallet) + } + + override suspend fun removeSession(userWallet: String, topic: String) { + val updatedList = loadSessions(userWallet).filterNot { it.topic == topic } + writeSessionToFile(sessions = updatedList, userWallet = userWallet) + } + + private fun writeSessionToFile(sessions: List, userWallet: String) { + val serialized = sessionsAdapter.toJson(sessions) + try { + fileReader.rewriteFile(serialized, getFileNameForUserWallet(userWallet)) + } catch (exception: Exception) { + Timber.e(exception) + } + } + + companion object { + private const val FILE_NAME_PREFIX = "wc_2" + + private fun getFileNameForUserWallet(userWallet: String): String { + return "$FILE_NAME_PREFIX-${userWallet.uppercase()}" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt new file mode 100644 index 0000000000..8cf7905260 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt @@ -0,0 +1,54 @@ +package com.tangem.tap.domain.walletconnect2.di + +import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper +import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper +import com.tangem.tap.domain.walletconnect2.app.WalletConnectEventsHandlerImpl +import com.tangem.tap.domain.walletconnect2.data.WalletConnectRepositoryImpl +import com.tangem.tap.domain.walletconnect2.data.WalletConnectSessionsRepositoryImpl +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository +import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(ActivityComponent::class) +class WalletConnectInteractorModule { + @Provides + @ActivityScoped + fun provideWalletConnectInteractor( + wcRepository: WalletConnectRepository, + wcSessionsRepository: WalletConnectSessionsRepository, + ): WalletConnectInteractor { + return WalletConnectInteractor( + handler = WalletConnectEventsHandlerImpl(), + walletConnectRepository = wcRepository, + sessionsRepository = wcSessionsRepository, + sdkHelper = WalletConnectSdkHelper(), + blockchainHelper = TangemWcBlockchainHelper(), + dispatcher = AppCoroutineDispatcherProvider(), + ) + } +} + +@Module +@InstallIn(SingletonComponent::class) +interface WalletConnectModule { + + @Binds + @Singleton + fun bindWalletConnectRepository(repository: WalletConnectRepositoryImpl): WalletConnectRepository + + @Binds + @Singleton + fun bindWalletConnectSessionsRepository( + repository: WalletConnectSessionsRepositoryImpl, + ): WalletConnectSessionsRepository +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectEventsHandler.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectEventsHandler.kt new file mode 100644 index 0000000000..30c73a0642 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectEventsHandler.kt @@ -0,0 +1,19 @@ +package com.tangem.tap.domain.walletconnect2.domain + +import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError +import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents +import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen + +interface WalletConnectEventsHandler { + fun onProposalReceived(proposal: WalletConnectEvents.SessionProposal, networksFormatted: String) + + fun onSessionEstablished() + + fun onSessionRejected(error: WalletConnectError) + + fun onListOfSessionsUpdated(sessions: List) + + fun onSessionRequest(request: WcPreparedRequest) + + fun onUnsupportedRequest() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt new file mode 100644 index 0000000000..03bd4e548e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -0,0 +1,224 @@ +package com.tangem.tap.domain.walletconnect2.domain + +import com.tangem.tap.common.extensions.* +import com.tangem.tap.domain.walletconnect.* +import com.tangem.tap.domain.walletconnect2.domain.models.* +import com.tangem.tap.features.details.ui.walletconnect.* +import com.tangem.utils.coroutines.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import timber.log.* + +class WalletConnectInteractor( + private val handler: WalletConnectEventsHandler, + private val walletConnectRepository: WalletConnectRepository, + private val sessionsRepository: WalletConnectSessionsRepository, + private val sdkHelper: WalletConnectSdkHelper, + private val dispatcher: CoroutineDispatcherProvider, + val blockchainHelper: WcBlockchainHelper, +) { + + private val events = walletConnectRepository.events + private val sessions = walletConnectRepository.activeSessions + + private var userWalletId: String = "" + private var cardId: String? = null + + private var currentRequest: WalletConnectEvents.SessionRequest? = null + private val sessionRequestConverter = WcSessionRequestConverter( + blockchainHelper = blockchainHelper, + sessionsRepository = sessionsRepository, + sdkHelper = sdkHelper, + ) + + suspend fun startListening(userWalletId: String, cardId: String?) { + this.userWalletId = userWalletId + this.cardId = cardId + coroutineScope { + launch { subscribeToEvents() } + launch { subscribeToSessions() } + } + walletConnectRepository.updateSessions() + } + + private suspend fun subscribeToEvents() { + events + .onEach { wcEvent -> + when (wcEvent) { + is WalletConnectEvents.SessionProposal -> { + Timber.d("WC session proposal event received") + val networksFormatted = wcEvent.chainIds + .mapNotNull { blockchainHelper.chainIdToFullNameOrNull(it) } + .toString() + handler.onProposalReceived(proposal = wcEvent, networksFormatted = networksFormatted) + } + is WalletConnectEvents.SessionApprovalError -> { + val error = when (wcEvent.error) { + is WalletConnectError.ApprovalErrorMissingNetworks -> { + val missingNetworks = wcEvent.error.missingChains + .map { blockchainHelper.chainIdToNetworkIdOrNull(it) } + val containsUnsupportedNetworks = missingNetworks.any { it == null } + if (containsUnsupportedNetworks) { + WalletConnectError.ApprovalErrorUnsupportedNetwork + } else { + WalletConnectError.ApprovalErrorAddNetwork(missingNetworks.filterNotNull()) + } + } + else -> wcEvent.error + } + handler.onSessionRejected(error) + } + is WalletConnectEvents.SessionApprovalSuccess -> { + sessionsRepository.saveSession( + userWallet = userWalletId, + session = Session.fromAccounts( + accounts = wcEvent.accounts, + topic = wcEvent.topic, + ), + ) + handler.onSessionEstablished() + } + is WalletConnectEvents.SessionDeleted -> { + sessionsRepository.removeSession(userWalletId, wcEvent.topic) + } + is WalletConnectEvents.SessionRequest -> { + handleRequest(wcEvent) + } + } + } + .flowOn(dispatcher.io) + .collect() + } + + private suspend fun subscribeToSessions() { + sessions + .onEach { listOfSessions -> + val relevantTopics = getTopicsForUserWallet(userWalletId, sessionsRepository) + val filteredSessions = filterSessionsForUserWallet(listOfSessions, relevantTopics) + handler.onListOfSessionsUpdated(filteredSessions) + } + .flowOn(dispatcher.io) + .collect() + } + + private fun filterSessionsForUserWallet( + availableSessions: List, + relevantTopics: List, + ): List { + return availableSessions.filter { relevantTopics.contains(it.topic) } + .map { + WcSessionForScreen( + description = it.name ?: "", + sessionId = it.topic, + ) + } + } + + private suspend fun getTopicsForUserWallet( + userWalletId: String, + repository: WalletConnectSessionsRepository, + ): List { + return repository.loadSessions(userWalletId).map { it.topic } + } + + fun approveSessionProposal(accounts: List) { + val userNamespaces: Map> = accounts + .groupBy { account -> + blockchainHelper.getNamespaceFromFullChainIdOrNull(account.chainId) + ?.let { NetworkNamespace(it) } + }.filterNotNull() + walletConnectRepository.approve( + userNamespaces = userNamespaces, + ) + } + + fun rejectSessionProposal() { + walletConnectRepository.reject() + } + + fun disconnectSession(topic: String) { + walletConnectRepository.disconnect(topic) + } + + fun rejectRequest(topic: String, id: Long) { + walletConnectRepository.rejectRequest(topic, id) + } + + private suspend fun handleRequest(sessionRequest: WalletConnectEvents.SessionRequest) { + val error: WalletConnectError? = when { + sessionsRepository.loadSessions(userWalletId).none { it.topic == sessionRequest.topic } -> { + WalletConnectError.WrongUserWallet + } + sessionRequest.request is WcRequest.CustomRequest -> { + WalletConnectError.UnsupportedMethod + } + else -> { + null + } + } + if (error != null) { + walletConnectRepository.rejectRequest(sessionRequest.topic, sessionRequest.id) + return + } + + when (sessionRequest.request) { + is WcRequest.BnbCancel -> Unit + is WcRequest.BnbTxConfirm -> walletConnectRepository.sendRequest( + topic = sessionRequest.topic, + id = sessionRequest.id, + result = "", + ) + else -> { + currentRequest = sessionRequest + val data = prepareRequestData(sessionRequest) + if (data != null) { + handler.onSessionRequest(data) + } + } + } + } + + suspend fun continueWithRequest(request: WcPreparedRequest) { + val currentRequest = this.currentRequest + if (currentRequest == null || request.topic != currentRequest.topic) return + + val networkId = blockchainHelper.chainIdToNetworkIdOrNull(currentRequest.chainId ?: "") ?: return + val signedHash = when (request) { + is WcPreparedRequest.BnbTransaction -> sdkHelper.signBnbTransaction( + data = request.preparedRequestData.data.data, + networkId = networkId, + derivationPath = request.derivationPath, + cardId = cardId, + ) + is WcPreparedRequest.EthTransaction -> sdkHelper.completeTransaction( + data = request.preparedRequestData, + cardId = cardId, + ) + is WcPreparedRequest.EthSign -> sdkHelper.signPersonalMessage( + hashToSign = request.preparedRequestData.hash, + networkId = networkId, + derivationPath = request.derivationPath, + cardId = cardId, + ) + } + + Timber.d("Signed hash: $signedHash") + + if (signedHash == null) { + walletConnectRepository.rejectRequest( + topic = request.topic, + id = request.requestId, + ) + } else { + walletConnectRepository.sendRequest( + topic = request.topic, + id = request.requestId, + result = signedHash, + ) + } + } + + private suspend fun prepareRequestData(sessionRequest: WalletConnectEvents.SessionRequest): WcPreparedRequest? { + return sessionRequestConverter.prepareRequest(sessionRequest, userWalletId) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt new file mode 100644 index 0000000000..4069a56fa8 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt @@ -0,0 +1,30 @@ +package com.tangem.tap.domain.walletconnect2.domain + +import com.tangem.tap.domain.walletconnect2.domain.models.Account +import com.tangem.tap.domain.walletconnect2.domain.models.NetworkNamespace +import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents +import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectSession +import kotlinx.coroutines.flow.Flow + +interface WalletConnectRepository { + + val events: Flow + + val activeSessions: Flow> + + fun init(projectId: String) + + fun updateSessions() + + fun pair(uri: String) + + fun disconnect(topic: String) + + fun approve(userNamespaces: Map>) + + fun reject() + + fun sendRequest(topic: String, id: Long, result: String) + + fun rejectRequest(topic: String, id: Long) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectSessionsRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectSessionsRepository.kt new file mode 100644 index 0000000000..395b928f86 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectSessionsRepository.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.domain.walletconnect2.domain + +import com.tangem.tap.domain.walletconnect2.domain.models.Session + +interface WalletConnectSessionsRepository { + suspend fun loadSessions(userWallet: String): List + + suspend fun saveSession(userWallet: String, session: Session) + + suspend fun removeSession(userWallet: String, topic: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcBlockchainHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcBlockchainHelper.kt new file mode 100644 index 0000000000..06f0843223 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcBlockchainHelper.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.domain.walletconnect2.domain + +interface WcBlockchainHelper { + fun chainIdToNetworkIdOrNull(chainId: String): String? + + fun networkIdToChainIdOrNull(networkId: String): String? + + fun getNamespaceFromFullChainIdOrNull(chainId: String): String? + + fun chainIdToFullNameOrNull(chainId: String): String? +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt new file mode 100644 index 0000000000..86ae71084c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt @@ -0,0 +1,209 @@ +package com.tangem.tap.domain.walletconnect2.domain + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonClass +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import com.tangem.datasource.di.SdkMoshi +import com.tangem.tap.domain.walletconnect2.domain.models.binance.WCBinanceTxConfirmParam +import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceCancelOrder +import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder +import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +enum class WcJrpcMethods(val code: String) { + + ETH_SIGN("eth_sign"), + ETH_PERSONAL_SIGN("personal_sign"), + ETH_SIGN_TYPE_DATA("eth_signTypedData"), + ETH_SIGN_TYPE_DATA_V4("eth_signTypedData_v4"), + ETH_SIGN_TRANSACTION("eth_signTransaction"), + ETH_SEND_TRANSACTION("eth_sendTransaction"), + BNB_SIGN("bnb_sign"), + BNB_TRANSACTION_CONFIRM("bnb_tx_confirmation"), + SIGN_TRANSACTION("trust_signTransaction"), + ; + + companion object { + fun fromCode(code: String): WcJrpcMethods? = values().firstOrNull { it.code == code } + } +} + +@JsonClass(generateAdapter = true) +data class WCSignTransaction( + val network: Int, + val transaction: String, +) : WcRequestData + +@JsonClass(generateAdapter = true) +data class WcEthereumSignMessage( + val raw: List, + val type: WCSignType, +) : WcRequestData { + enum class WCSignType { + MESSAGE, PERSONAL_MESSAGE, TYPED_MESSAGE + } + + /** + * Raw parameters will always be the message and the address. Depending on the WCSignType, + * those parameters can be swapped as description below: + * + * - MESSAGE: `[address, data ]` + * - TYPED_MESSAGE: `[address, data]` + * - PERSONAL_MESSAGE: `[data, address]` + * + * reference: https://docs.walletconnect.org/json-rpc/ethereum#eth_signtypeddata + */ + val data + get() = when (type) { + WCSignType.PERSONAL_MESSAGE -> raw[0] + else -> raw[1] + } + + val address + get() = when (type) { + WCSignType.PERSONAL_MESSAGE -> raw[1] + else -> raw[0] + } +} + +@JsonClass(generateAdapter = true) +data class WcEthereumTransaction( + val from: String, + val to: String?, + val nonce: String?, + val gasPrice: String?, + val maxFeePerGas: String?, + val maxPriorityFeePerGas: String?, + val gas: String?, + val gasLimit: String?, + val value: String?, + val data: String, +) : WcRequestData + +interface WcRequestData + +data class WcCustomRequestData(val data: String) : WcRequestData + +sealed class WcRequest(open val data: WcRequestData) { + data class EthSign(override val data: WcEthereumSignMessage) : WcRequest(data) + data class EthSignTransaction(override val data: WcEthereumTransaction) : WcRequest(data) + data class EthSendTransaction(override val data: WcEthereumTransaction) : WcRequest(data) + data class BnbTrade(override val data: WcBinanceTradeOrder) : WcRequest(data) + data class BnbCancel(override val data: WcBinanceCancelOrder) : WcRequest(data) + data class BnbTransfer(override val data: WcBinanceTransferOrder) : WcRequest(data) + data class BnbTxConfirm(override val data: WCBinanceTxConfirmParam) : WcRequest(data) + data class SignTransaction(override val data: WCSignTransaction) : WcRequest(data) + data class CustomRequest(override val data: WcCustomRequestData) : WcRequest(data) +} + +@Singleton +class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private val moshi: Moshi) { + + @Suppress("ComplexMethod", "LongMethod") + fun deserialize(method: String, params: String): WcRequest { + val customRequest = WcRequest.CustomRequest(WcCustomRequestData(params)) + val wcMethod: WcJrpcMethods = WcJrpcMethods.fromCode(method) ?: return customRequest + + return when (wcMethod) { + WcJrpcMethods.ETH_SIGN_TRANSACTION -> { + val deserializedParams = moshi.adapter>( + Types.newParameterizedType(List::class.java, WcEthereumTransaction::class.java), + ).fromJsonFirstOrNull(params) ?: return customRequest + WcRequest.EthSignTransaction(data = deserializedParams) + } + WcJrpcMethods.ETH_SEND_TRANSACTION -> { + val deserializedParams = moshi.adapter>( + Types.newParameterizedType(List::class.java, WcEthereumTransaction::class.java), + ).fromJsonFirstOrNull(params) ?: return customRequest + WcRequest.EthSendTransaction(data = deserializedParams) + } + WcJrpcMethods.ETH_SIGN -> { + val deserializedParams = moshi.adapter>( + Types.newParameterizedType(List::class.java, String::class.java), + ).fromJsonOrNull(params) ?: return customRequest + val data = WcEthereumSignMessage( + raw = deserializedParams, + type = WcEthereumSignMessage.WCSignType.MESSAGE, + ) + WcRequest.EthSign(data = data) + } + WcJrpcMethods.ETH_PERSONAL_SIGN -> { + val deserializedParams = moshi.adapter>( + Types.newParameterizedType(List::class.java, String::class.java), + ).fromJsonOrNull(params) ?: return customRequest + val data = WcEthereumSignMessage( + raw = deserializedParams, + type = WcEthereumSignMessage.WCSignType.PERSONAL_MESSAGE, + ) + WcRequest.EthSign(data = data) + } + WcJrpcMethods.ETH_SIGN_TYPE_DATA, WcJrpcMethods.ETH_SIGN_TYPE_DATA_V4 -> { + val deserializedParams = listOf( + params.substring(params.indexOf("\"") + 1, params.indexOf("\"", startIndex = 2)), + params.substring(params.indexOfFirst { it == '{' }, params.indexOfLast { it == '}' } + 1), + ) + val data = WcEthereumSignMessage( + deserializedParams, + WcEthereumSignMessage.WCSignType.TYPED_MESSAGE, + ) + Timber.d("TypedData params: $deserializedParams") + WcRequest.EthSign(data) + } + WcJrpcMethods.BNB_SIGN -> { + return deserializeBnb(moshi, params) ?: customRequest + } + WcJrpcMethods.BNB_TRANSACTION_CONFIRM -> { + val deserializedParams = moshi.adapter>( + Types.newParameterizedType(List::class.java, WCBinanceTxConfirmParam::class.java), + ).fromJsonFirstOrNull(params) ?: return customRequest + WcRequest.BnbTxConfirm(data = deserializedParams) + } + WcJrpcMethods.SIGN_TRANSACTION -> { + val deserializedParams = moshi.adapter>( + Types.newParameterizedType(List::class.java, WCSignTransaction::class.java), + ).fromJsonFirstOrNull(params) ?: return customRequest + WcRequest.SignTransaction(data = deserializedParams) + } + } + } + + private fun deserializeBnb(moshi: Moshi, params: String): WcRequest? { + val cancelOrder = moshi.adapter>( + Types.newParameterizedType(List::class.java, WcBinanceCancelOrder::class.java), + ).fromJsonFirstOrNull(params) + if (cancelOrder != null) return WcRequest.BnbCancel(cancelOrder) + + val tradeOrder = moshi.adapter>( + Types.newParameterizedType(List::class.java, WcBinanceTradeOrder::class.java), + ).fromJsonFirstOrNull(params) + if (tradeOrder != null) return WcRequest.BnbTrade(tradeOrder) + + val transferOrder = moshi.adapter>( + Types.newParameterizedType(List::class.java, WcBinanceTransferOrder::class.java), + ).fromJsonFirstOrNull(params) + if (transferOrder != null) return WcRequest.BnbTransfer(transferOrder) + + return null + } + + private fun JsonAdapter.fromJsonOrNull(data: String): T? { + return try { + this.fromJson(data) + } catch (e: Exception) { + Timber.e(e.message) + null + } + } + + private fun JsonAdapter>.fromJsonFirstOrNull(data: String): T? { + return try { + this.fromJson(data)?.firstOrNull() + } catch (e: Exception) { + Timber.e(e.message) + null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcPreparedRequest.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcPreparedRequest.kt new file mode 100644 index 0000000000..be17cc867b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcPreparedRequest.kt @@ -0,0 +1,33 @@ +package com.tangem.tap.domain.walletconnect2.domain + +import com.tangem.tap.domain.walletconnect2.domain.models.BnbData +import com.tangem.tap.features.details.redux.walletconnect.WcPersonalSignData +import com.tangem.tap.features.details.redux.walletconnect.WcTransactionData + +sealed class WcPreparedRequest( + open val preparedRequestData: Any, + val topic: String, + val requestId: Long, + val derivationPath: String?, +) { + class EthSign( + override val preparedRequestData: WcPersonalSignData, + topic: String, + requestId: Long, + derivationPath: String?, + ) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath) + + class EthTransaction( + override val preparedRequestData: WcTransactionData, + topic: String, + requestId: Long, + derivationPath: String?, + ) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath) + + class BnbTransaction( + override val preparedRequestData: BnbData, + topic: String, + requestId: Long, + derivationPath: String?, + ) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt new file mode 100644 index 0000000000..bc93e31462 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt @@ -0,0 +1,136 @@ +package com.tangem.tap.domain.walletconnect2.domain + +import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper +import com.tangem.tap.domain.walletconnect2.domain.models.BnbData +import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData +import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents +import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType + +class WcSessionRequestConverter( + private val blockchainHelper: WcBlockchainHelper, + private val sessionsRepository: WalletConnectSessionsRepository, + private val sdkHelper: WalletConnectSdkHelper, +) { + + @Suppress("LongMethod") + suspend fun prepareRequest( + sessionRequest: WalletConnectEvents.SessionRequest, + userWalletId: String, + ): WcPreparedRequest? { + val networkId = blockchainHelper.chainIdToNetworkIdOrNull(sessionRequest.chainId ?: "") ?: return null + val derivationPath = getDerivationPath( + sessionsRepository = sessionsRepository, + sessionRequest = sessionRequest, + userWalletId = userWalletId, + walletAddress = getWalletAddress(sessionRequest.request), + ) + return when (val request = sessionRequest.request) { + is WcRequest.EthSendTransaction -> { + val data = sdkHelper.prepareTransactionData( + EthTransactionData( + transaction = request.data, + networkId = networkId, + rawDerivationPath = derivationPath, + id = sessionRequest.id, + topic = sessionRequest.topic, + type = WcEthTransactionType.EthSendTransaction, + metaName = sessionRequest.metaName, + metaUrl = sessionRequest.metaUrl, + ), + ) ?: return null + WcPreparedRequest.EthTransaction( + preparedRequestData = data, + topic = sessionRequest.topic, + requestId = sessionRequest.id, + derivationPath = derivationPath, + ) + } + is WcRequest.EthSignTransaction -> { + val data = sdkHelper.prepareTransactionData( + EthTransactionData( + transaction = request.data, + networkId = networkId, + rawDerivationPath = derivationPath, + id = sessionRequest.id, + topic = sessionRequest.topic, + type = WcEthTransactionType.EthSignTransaction, + metaName = sessionRequest.metaName, + metaUrl = sessionRequest.metaUrl, + ), + ) ?: return null + WcPreparedRequest.EthTransaction( + preparedRequestData = data, + topic = sessionRequest.topic, + requestId = sessionRequest.id, + derivationPath = derivationPath, + ) + } + is WcRequest.EthSign -> { + val data = sdkHelper.prepareDataForPersonalSign( + request.data, + sessionRequest.topic, + sessionRequest.metaName, + sessionRequest.id, + ) + WcPreparedRequest.EthSign( + preparedRequestData = data, + topic = sessionRequest.topic, + requestId = sessionRequest.id, + derivationPath = derivationPath, + ) + } + is WcRequest.BnbTrade -> { + val data = sdkHelper.prepareBnbTradeOrder(request.data) + WcPreparedRequest.BnbTransaction( + BnbData( + data = data, + topic = sessionRequest.topic, + requestId = sessionRequest.id, + dAppName = sessionRequest.metaName, + ), + topic = sessionRequest.topic, + requestId = sessionRequest.id, + derivationPath = derivationPath, + ) + } + is WcRequest.BnbTransfer -> { + val data = sdkHelper.prepareBnbTransferOrder(request.data) + WcPreparedRequest.BnbTransaction( + BnbData( + data = data, + topic = sessionRequest.topic, + requestId = sessionRequest.id, + dAppName = sessionRequest.metaName, + ), + topic = sessionRequest.topic, + requestId = sessionRequest.id, + derivationPath = derivationPath, + ) + } + else -> null + } + } + + private fun getWalletAddress(request: WcRequest): String? { + return when (request) { + is WcRequest.BnbTrade -> request.data.accountNumber + is WcRequest.BnbTransfer -> request.data.accountNumber + is WcRequest.EthSendTransaction -> request.data.from + is WcRequest.EthSignTransaction -> request.data.from + is WcRequest.EthSign -> request.data.address + else -> null + } + } + + private suspend fun getDerivationPath( + sessionsRepository: WalletConnectSessionsRepository, + sessionRequest: WalletConnectEvents.SessionRequest, + userWalletId: String, + walletAddress: String?, + ): String? { + return sessionsRepository.loadSessions(userWalletId) + .firstOrNull { it.topic == sessionRequest.topic } + ?.accounts?.firstOrNull { it.chainId == sessionRequest.chainId && it.walletAddress == walletAddress } + ?.derivationPath + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Account.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Account.kt new file mode 100644 index 0000000000..9372666b46 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Account.kt @@ -0,0 +1,3 @@ +package com.tangem.tap.domain.walletconnect2.domain.models + +data class Account(val chainId: String, val walletAddress: String, val derivationPath: String?) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/BnbData.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/BnbData.kt new file mode 100644 index 0000000000..194ca5d01d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/BnbData.kt @@ -0,0 +1,10 @@ +package com.tangem.tap.domain.walletconnect2.domain.models + +import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData + +data class BnbData( + val data: BinanceMessageData, + val topic: String, + val requestId: Long, + val dAppName: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/ChainWithDerivation.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/ChainWithDerivation.kt new file mode 100644 index 0000000000..8f43892142 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/ChainWithDerivation.kt @@ -0,0 +1,6 @@ +package com.tangem.tap.domain.walletconnect2.domain.models + +data class ChainWithDerivation( + val chain: String, + val derivationPath: String?, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/EthTransactionData.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/EthTransactionData.kt new file mode 100644 index 0000000000..2138d6bd63 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/EthTransactionData.kt @@ -0,0 +1,15 @@ +package com.tangem.tap.domain.walletconnect2.domain.models + +import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction +import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType + +data class EthTransactionData( + val transaction: WcEthereumTransaction, + val networkId: String, + val rawDerivationPath: String?, + val id: Long, + val topic: String, + val type: WcEthTransactionType, + val metaName: String, + val metaUrl: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/NetworkNamespace.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/NetworkNamespace.kt new file mode 100644 index 0000000000..a431210e42 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/NetworkNamespace.kt @@ -0,0 +1,4 @@ +package com.tangem.tap.domain.walletconnect2.domain.models + +@JvmInline +value class NetworkNamespace(val value: String) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Session.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Session.kt new file mode 100644 index 0000000000..4637a99acb --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Session.kt @@ -0,0 +1,15 @@ +package com.tangem.tap.domain.walletconnect2.domain.models + +data class Session( + val topic: String, + val accounts: List, +) { + companion object { + fun fromAccounts(accounts: List, topic: String): Session { + return Session( + topic = topic, + accounts = accounts, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt new file mode 100644 index 0000000000..1f9b7f6771 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt @@ -0,0 +1,10 @@ +package com.tangem.tap.domain.walletconnect2.domain.models + +sealed class WalletConnectError : Exception() { + data class ApprovalErrorMissingNetworks(val missingChains: List) : WalletConnectError() + data class ApprovalErrorAddNetwork(val networks: List) : WalletConnectError() + object ApprovalErrorUnsupportedNetwork : WalletConnectError() + data class ExternalApprovalError(override val message: String?) : WalletConnectError() + object WrongUserWallet : WalletConnectError() + object UnsupportedMethod : WalletConnectError() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt new file mode 100644 index 0000000000..bec06dd554 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.domain.walletconnect2.domain.models + +import com.tangem.tap.domain.walletconnect2.domain.WcRequest +import java.net.URI + +sealed interface WalletConnectEvents { + data class SessionProposal( + val name: String, + val description: String, + val url: String, + val icons: List, + val chainIds: List, + ) : WalletConnectEvents + + data class SessionApprovalError(val error: WalletConnectError) : WalletConnectEvents + data class SessionApprovalSuccess(val topic: String, val accounts: List) : WalletConnectEvents + data class SessionDeleted(val topic: String) : WalletConnectEvents + + data class SessionRequest( + val request: WcRequest, + val chainId: String?, + val topic: String, + val id: Long, + val metaName: String, + val metaUrl: String, + ) : WalletConnectEvents +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectSession.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectSession.kt new file mode 100644 index 0000000000..fb535bcc66 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectSession.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.domain.walletconnect2.domain.models + +data class WalletConnectSession( + val topic: String, + val icon: String?, + val name: String?, + val url: String?, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceCancelOrder.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceCancelOrder.kt new file mode 100644 index 0000000000..5d5cc625d2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceCancelOrder.kt @@ -0,0 +1,35 @@ +package com.tangem.tap.domain.walletconnect2.domain.models.binance + +import com.squareup.moshi.* + +@Suppress("LongParameterList") +@JsonClass(generateAdapter = true) +class WcBinanceCancelOrder( + @Json(name = "account_number") + accountNumber: String, + @Json(name = "chain_id") + chainId: String, + @Json(name = "data") + data: String?, + @Json(name = "memo") + memo: String?, + @Json(name = "sequence") + sequence: String, + @Json(name = "source") + source: String, + @Json(name = "msgs") + msgs: List, +) : WcBinanceOrder(accountNumber, chainId, data, memo, sequence, source, msgs) { + + enum class MessageKey(val key: String) { + REFID("refid"), + SENDER("sender"), + SYMBOL("symbol"), + } + + data class Message( + val refid: String, + val sender: String, + val symbol: String, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceOrder.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceOrder.kt new file mode 100644 index 0000000000..8325aadd72 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceOrder.kt @@ -0,0 +1,19 @@ +package com.tangem.tap.domain.walletconnect2.domain.models.binance + +import com.tangem.tap.domain.walletconnect2.domain.WcRequestData + +@Suppress("LongParameterList") +open class WcBinanceOrder( + val accountNumber: String, + val chainId: String, + val data: String?, + val memo: String?, + val sequence: String, + val source: String, + val msgs: List, +) : WcRequestData + +data class WCBinanceTxConfirmParam( + val ok: Boolean, + val errorMsg: String?, +) : WcRequestData \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTradeOrder.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTradeOrder.kt new file mode 100644 index 0000000000..35456d2eb5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTradeOrder.kt @@ -0,0 +1,70 @@ +package com.tangem.tap.domain.walletconnect2.domain.models.binance + +import com.github.salomonbrys.kotson.* +import com.google.gson.JsonObject +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@Suppress("LongParameterList") +@JsonClass(generateAdapter = true) +class WcBinanceTradeOrder( + @Json(name = "account_number") + accountNumber: String, + @Json(name = "chain_id") + chainId: String, + data: String?, + memo: String?, + sequence: String, + source: String, + msgs: List, +) : WcBinanceOrder(accountNumber, chainId, data, memo, sequence, source, msgs) { + + enum class MessageKey(val key: String) { + ID("id"), + ORDER_TYPE("ordertype"), + PRICE("price"), + QUANTITY("quantity"), + SENDER("sender"), + SIDE("side"), + SYMBOL("symbol"), + TIME_INFORCE("timeinforce"), + } + + data class Message( + val id: String, + val orderType: Int, + val price: Long, + val quantity: Long, + val sender: String, + val side: Int, + val symbol: String, + val timeInforce: Int, + ) +} + +val tradeOrderDeserializer = jsonDeserializer { + WcBinanceTradeOrder.Message( + id = it.json[WcBinanceTradeOrder.MessageKey.ID.key].string, + orderType = it.json[WcBinanceTradeOrder.MessageKey.ORDER_TYPE.key].int, + price = it.json[WcBinanceTradeOrder.MessageKey.PRICE.key].long, + quantity = it.json[WcBinanceTradeOrder.MessageKey.QUANTITY.key].long, + sender = it.json[WcBinanceTradeOrder.MessageKey.SENDER.key].string, + side = it.json[WcBinanceTradeOrder.MessageKey.SIDE.key].int, + symbol = it.json[WcBinanceTradeOrder.MessageKey.SYMBOL.key].string, + timeInforce = it.json[WcBinanceTradeOrder.MessageKey.TIME_INFORCE.key].int, + ) +} + +val tradeOrderSerializer = jsonSerializer { + val jsonObject = JsonObject() + jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.ID.key, it.src.id) + jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.ORDER_TYPE.key, it.src.orderType) + jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.PRICE.key, it.src.price) + jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.QUANTITY.key, it.src.quantity) + jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.SENDER.key, it.src.sender) + jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.SIDE.key, it.src.side) + jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.SYMBOL.key, it.src.symbol) + jsonObject.addProperty(WcBinanceTradeOrder.MessageKey.TIME_INFORCE.key, it.src.timeInforce) + + jsonObject +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTradePair.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTradePair.kt new file mode 100644 index 0000000000..54bf23dcaf --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTradePair.kt @@ -0,0 +1,17 @@ +package com.tangem.tap.domain.walletconnect2.domain.models.binance + +data class WcBinanceTradePair(val from: String, val to: String) { + companion object { + fun from(symbol: String): WcBinanceTradePair? { + val pair = symbol.split("_") + + return if (pair.size > 1) { + val firstParts = pair[0].split("-") + val secondParts = pair[1].split("-") + WcBinanceTradePair(firstParts[0], secondParts[0]) + } else { + null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTransferOrder.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTransferOrder.kt new file mode 100644 index 0000000000..381069071e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/binance/WcBinanceTransferOrder.kt @@ -0,0 +1,41 @@ +package com.tangem.tap.domain.walletconnect2.domain.models.binance + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@Suppress("LongParameterList") +@JsonClass(generateAdapter = true) +class WcBinanceTransferOrder( + @Json(name = "account_number") + accountNumber: String, + @Json(name = "chain_id") + chainId: String, + data: String?, + memo: String?, + sequence: String, + source: String, + msgs: List, +) : WcBinanceOrder(accountNumber, chainId, data, memo, sequence, source, msgs) { + + enum class MessageKey(val key: String) { + INPUTS("inputs"), + OUTPUTS("outputs"), + } + + data class Message( + val inputs: List, + val outputs: List, + ) { + + data class Item( + val address: String, + val coins: List, + ) { + + data class Coin( + val amount: Long, + val denom: String, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt index 9afb96f10f..6a773800b6 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt @@ -4,6 +4,10 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.redux.NotificationAction import com.tangem.tap.domain.TapError +import com.tangem.tap.domain.walletconnect.Topic +import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest +import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError +import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen import com.tangem.wallet.R import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder @@ -58,7 +62,7 @@ sealed class WalletConnectAction : Action { data class SetSessionsRestored(val sessions: List) : WalletConnectAction() - data class DisconnectSession(val session: WCSession) : WalletConnectAction() + data class DisconnectSession(val topic: String, val session: WCSession?) : WalletConnectAction() data class RemoveSession(val session: WCSession) : WalletConnectAction() @@ -66,7 +70,7 @@ sealed class WalletConnectAction : Action { val transaction: WCEthereumTransaction, val session: WalletConnectSession, val id: Long, - val type: WcTransactionType, + val type: WcEthTransactionType, ) : WalletConnectAction() @@ -78,11 +82,11 @@ sealed class WalletConnectAction : Action { val id: Long, ) : WalletConnectAction() - data class SendTransaction(val session: WCSession) : WalletConnectAction() + data class SendTransaction(val topic: Topic) : WalletConnectAction() - data class SignMessage(val session: WCSession) : WalletConnectAction() + data class SignMessage(val topic: Topic) : WalletConnectAction() - data class RejectRequest(val session: WCSession, val id: Long) : WalletConnectAction() + data class RejectRequest(val topic: Topic, val id: Long) : WalletConnectAction() object NotEnoughFunds : WalletConnectAction(), NotificationAction { override val messageResource = R.string.wallet_connect_create_tx_not_enough_funds @@ -108,7 +112,22 @@ sealed class WalletConnectAction : Action { data class Sign( val id: Long, val data: ByteArray, - val sessionData: WCSession, + val topic: Topic, ) : WalletConnectAction() } + + //region WalletConnect 2.0 + object ApproveProposal : WalletConnectAction() + object RejectProposal : WalletConnectAction() + + object SessionEstablished : WalletConnectAction() + data class SessionRejected(val error: WalletConnectError) : WalletConnectAction() + data class SessionListUpdated(val sessions: List) : WalletConnectAction() + + data class ShowSessionRequest(val sessionRequest: WcPreparedRequest) : WalletConnectAction() + + object RejectUnsupportedRequest : WalletConnectAction() + + data class PerformRequestedAction(val sessionRequest: WcPreparedRequest) : WalletConnectAction() + //endregion WalletConnect 2.0 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index 65dfeb1eab..b83b2ad75a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -5,8 +5,10 @@ import com.tangem.blockchain.common.DerivationStyle import com.tangem.blockchain.common.WalletManager import com.tangem.common.extensions.guard import com.tangem.domain.common.TapWorkarounds.derivationStyle -import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.extensions.withMainContext +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.extensions.dispatchOnMain @@ -19,8 +21,16 @@ import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.domain.walletconnect.BnbHelper import com.tangem.tap.domain.walletconnect.WalletConnectManager import com.tangem.tap.domain.walletconnect.WalletConnectNetworkUtils +import com.tangem.tap.domain.walletconnect.extensions.toWcEthTransaction +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository +import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest +import com.tangem.tap.domain.walletconnect2.domain.models.Account +import com.tangem.tap.domain.walletconnect2.domain.models.BnbData +import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.wallet.R @@ -29,8 +39,14 @@ import org.rekotlin.Action import org.rekotlin.Middleware import timber.log.Timber +@Suppress("LargeClass") class WalletConnectMiddleware { private var walletConnectManager = WalletConnectManager() + private val walletConnectInteractor: WalletConnectInteractor + get() = store.state.daggerGraphState.get(DaggerGraphState::walletConnectInteractor) + private val walletConnectRepository: WalletConnectRepository + get() = store.state.daggerGraphState.get(DaggerGraphState::walletConnectRepository) + val walletConnectMiddleware: Middleware = { dispatch, state -> { next -> { action -> @@ -115,9 +131,13 @@ class WalletConnectMiddleware { store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedCard)) } is WalletConnectAction.OpenSession -> { - walletConnectManager.connect( - wcUri = action.wcUri, - ) + val index = action.wcUri.indexOf("@") + when (action.wcUri[index + 1]) { + '1' -> { + walletConnectManager.connect(wcUri = action.wcUri) + } + '2' -> walletConnectRepository.pair(uri = action.wcUri) + } } is WalletConnectAction.RefuseOpeningSession -> { store.dispatch( @@ -134,11 +154,15 @@ class WalletConnectMiddleware { walletConnectManager.approve(action.session) } is WalletConnectAction.DisconnectSession -> { - walletConnectManager.disconnect(action.session) + if (action.session != null) { + walletConnectManager.disconnect(action.session) + } else { + walletConnectInteractor.disconnectSession(action.topic) + } } is WalletConnectAction.HandleTransactionRequest -> { walletConnectManager.handleTransactionRequest( - transaction = action.transaction, + transaction = action.transaction.toWcEthTransaction(), session = action.session, id = action.id, type = action.type, @@ -152,23 +176,31 @@ class WalletConnectMiddleware { ) } is WalletConnectAction.RejectRequest -> { - walletConnectManager.rejectRequest(action.session, action.id) + walletConnectManager.rejectRequest(action.topic, action.id) + walletConnectInteractor.rejectRequest(action.topic, action.id) } is WalletConnectAction.SendTransaction -> { - walletConnectManager.completeTransaction(action.session) + walletConnectManager.completeTransaction(action.topic) } is WalletConnectAction.SignMessage -> { - walletConnectManager.sendSignedMessage(action.session) + walletConnectManager.sendSignedMessage(action.topic) } is WalletConnectAction.BinanceTransaction.Trade -> { val messageData = BnbHelper.createMessageData(action.order) store.dispatchOnMain( GlobalAction.ShowDialog( WalletConnectDialog.BnbTransactionDialog( - data = messageData, - session = action.sessionData.session, - sessionId = action.id, - dAppName = action.sessionData.peerMeta.name, + WcPreparedRequest.BnbTransaction( + preparedRequestData = BnbData( + data = messageData, + topic = action.sessionData.session.topic, + requestId = action.id, + dAppName = action.sessionData.peerMeta.name, + ), + topic = action.sessionData.session.topic, + requestId = action.id, + derivationPath = action.sessionData.wallet.derivationPath?.rawPath, + ), ), ), ) @@ -178,10 +210,17 @@ class WalletConnectMiddleware { store.dispatchOnMain( GlobalAction.ShowDialog( WalletConnectDialog.BnbTransactionDialog( - data = messageData, - session = action.sessionData.session, - sessionId = action.id, - dAppName = action.sessionData.peerMeta.name, + WcPreparedRequest.BnbTransaction( + preparedRequestData = BnbData( + data = messageData, + topic = action.sessionData.session.topic, + requestId = action.id, + dAppName = action.sessionData.peerMeta.name, + ), + topic = action.sessionData.session.topic, + requestId = action.id, + derivationPath = action.sessionData.wallet.derivationPath?.rawPath, + ), ), ), ) @@ -190,7 +229,7 @@ class WalletConnectMiddleware { walletConnectManager.signBnb( id = action.id, data = action.data, - sessionData = action.sessionData, + topic = action.topic, ) } is WalletConnectAction.SwitchBlockchain -> { @@ -226,6 +265,70 @@ class WalletConnectMiddleware { is WalletConnectAction.UpdateBlockchain -> { walletConnectManager.updateBlockchain(action.updatedSession) } + is WalletConnectAction.ApproveProposal -> { + val accounts = store.state.walletState.walletManagers + .mapNotNull { + val wallet = it.wallet + val chainId = walletConnectInteractor.blockchainHelper.networkIdToChainIdOrNull( + wallet.blockchain.toNetworkId(), + ) + chainId?.let { + Account( + chainId, + wallet.address, + wallet.publicKey.derivationPath?.rawPath, + ) + } + } + + walletConnectInteractor.approveSessionProposal(accounts) + } + is WalletConnectAction.RejectProposal -> { + walletConnectInteractor.rejectSessionProposal() + } + is WalletConnectAction.SessionEstablished -> { + } + is WalletConnectAction.SessionRejected -> { + when (action.error) { + is WalletConnectError.ApprovalErrorAddNetwork -> { + val blockchains = action.error.networks.mapNotNull { Blockchain.fromNetworkId(it)?.fullName } + store.dispatchOnMain( + GlobalAction.ShowDialog( + WalletConnectDialog.AddNetwork(blockchains.toString()), + ), + ) + } + + WalletConnectError.ApprovalErrorUnsupportedNetwork -> { + store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork)) + } + is WalletConnectError.ExternalApprovalError -> { + store.dispatchOnMain( + GlobalAction.ShowDialog( + AppDialog.SimpleOkWarningDialog( + message = action.error.message ?: "", + ), + ), + ) + } + else -> Unit + } + } + + is WalletConnectAction.ShowSessionRequest -> { + val dialog: WalletConnectDialog = when (val request = action.sessionRequest) { + is WcPreparedRequest.BnbTransaction -> WalletConnectDialog.BnbTransactionDialog(request) + is WcPreparedRequest.EthTransaction -> WalletConnectDialog.RequestTransaction(request) + is WcPreparedRequest.EthSign -> WalletConnectDialog.PersonalSign(request) + } + store.dispatch(GlobalAction.ShowDialog(dialog)) + } + is WalletConnectAction.PerformRequestedAction -> { + scope.launch { walletConnectInteractor.continueWithRequest(action.sessionRequest) } + } + is WalletConnectAction.RejectUnsupportedRequest -> { + store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork)) + } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt index ed1573f3bb..44130862c0 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt @@ -27,15 +27,23 @@ object WalletConnectReducer { state.sessions.filterNot { it.session.toUri() == action.session.toUri() } state.copy(sessions = sessions) } - is WalletConnectAction.UnsupportedCard -> state.copy(loading = false) - is WalletConnectAction.RefuseOpeningSession -> state.copy(loading = false) - is WalletConnectAction.OpeningSessionTimeout -> state.copy(loading = false) - is WalletConnectAction.FailureEstablishingSession -> state.copy(loading = false) + is WalletConnectAction.UnsupportedCard, + is WalletConnectAction.RefuseOpeningSession, + is WalletConnectAction.OpeningSessionTimeout, + is WalletConnectAction.FailureEstablishingSession, + -> state.copy(loading = false) is WalletConnectAction.UpdateBlockchain -> state.copy( sessions = state.sessions .filterNot { it.peerId == action.updatedSession.peerId } + action.updatedSession, ) - + is WalletConnectAction.ApproveProposal -> state.copy(loading = true) + is WalletConnectAction.RejectProposal, + is WalletConnectAction.SessionEstablished, + is WalletConnectAction.SessionRejected, + -> state.copy(loading = false) + is WalletConnectAction.SessionListUpdated -> state.copy( + wc2Sessions = action.sessions, + ) else -> state } } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt index 11bb3e6a8b..71f4a14ca6 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt @@ -8,6 +8,10 @@ import com.tangem.blockchain.common.WalletManager import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.redux.StateDialog +import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage +import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest +import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents +import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData import com.trustwallet.walletconnect.models.WCPeerMeta @@ -16,6 +20,7 @@ import com.trustwallet.walletconnect.models.session.WCSession data class WalletConnectState( val loading: Boolean = false, val sessions: List = listOf(), + val wc2Sessions: List = listOf(), val newSessionData: NewWcSessionData? = null, ) @@ -95,43 +100,47 @@ sealed class WalletConnectDialog : StateDialog { val networks: List, ) : WalletConnectDialog() + data class SessionProposalDialog( + val sessionProposal: WalletConnectEvents.SessionProposal, + val networks: String, + val onApprove: () -> Unit, + val onReject: () -> Unit, + ) : WalletConnectDialog() + data class ChooseNetwork( val session: WalletConnectSession, val networks: List, ) : WalletConnectDialog() - data class RequestTransaction(val dialogData: TransactionRequestDialogData) : + data class RequestTransaction(val data: WcPreparedRequest.EthTransaction) : WalletConnectDialog() - data class PersonalSign(val data: PersonalSignDialogData) : WalletConnectDialog() + data class PersonalSign(val data: WcPreparedRequest.EthSign) : WalletConnectDialog() data class BnbTransactionDialog( - val data: BinanceMessageData, - val session: WCSession, - val sessionId: Long, - val dAppName: String, + val data: WcPreparedRequest.BnbTransaction, ) : WalletConnectDialog() } data class WcTransactionData( - val type: WcTransactionType, + val type: WcEthTransactionType, val transaction: TransactionData, - val session: WalletConnectSession, + val topic: String, val id: Long, val walletManager: WalletManager, val dialogData: TransactionRequestDialogData, ) -enum class WcTransactionType { +enum class WcEthTransactionType { EthSignTransaction, EthSendTransaction, } data class WcPersonalSignData( val hash: ByteArray, - val session: WalletConnectSession, + val topic: String, val id: Long, val dialogData: PersonalSignDialogData, - + val type: WcEthereumSignMessage.WCSignType, ) sealed class BinanceMessageData( diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt index ba234154d5..c9f4292991 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt @@ -1,24 +1,11 @@ package com.tangem.tap.features.details.ui.walletconnect import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.FloatingActionButton -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.LinearProgressIndicator -import androidx.compose.material.Text +import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -36,6 +23,7 @@ import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.getFromClipboard import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R +import kotlinx.collections.immutable.persistentListOf @Composable fun WalletConnectScreen(state: WalletConnectScreenState, onBackClick: () -> Unit) { @@ -164,7 +152,7 @@ private fun WalletConnectSessions(state: WalletConnectScreenState) { private fun WalletConnectScreenPreview() { WalletConnectScreen( state = WalletConnectScreenState( - sessions = listOf( + sessions = persistentListOf( WcSessionForScreen( description = "session from some dApp", sessionId = "", diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt index e61b4c22e6..24878f3183 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt @@ -1,9 +1,10 @@ package com.tangem.tap.features.details.ui.walletconnect import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession +import kotlinx.collections.immutable.ImmutableList data class WalletConnectScreenState( - val sessions: List, + val sessions: ImmutableList, val isLoading: Boolean = false, val onRemoveSession: (String) -> Unit = {}, val onAddSession: (String?) -> Unit = {}, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt index 6bbde4c1ae..4707547c51 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt @@ -4,23 +4,35 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState +import kotlinx.collections.immutable.toImmutableList import org.rekotlin.Store +import timber.log.Timber class WalletConnectViewModel(private val store: Store) { fun updateState(state: WalletConnectState): WalletConnectScreenState { + Timber.d("WC2 Sessions: ${state.wc2Sessions}") + val sessions = state.sessions.map { wcSession -> WcSessionForScreen.fromSession(wcSession) } + state.wc2Sessions return WalletConnectScreenState( - state.sessions.map { wcSession -> WcSessionForScreen.fromSession(wcSession) }, + sessions.toImmutableList(), isLoading = state.loading, - onRemoveSession = { sessionUri -> onRemoveSession(sessionUri, state.sessions) }, + onRemoveSession = { sessionUri -> onRemoveSession(sessionUri, state.sessions, state.wc2Sessions) }, onAddSession = { copiedUri -> store.dispatch(WalletConnectAction.StartWalletConnect(copiedUri)) }, ) } - private fun onRemoveSession(sessionUri: String, sessions: List) { + private fun onRemoveSession( + sessionUri: String, + sessions: List, + wc2sessions: List, + ) { sessions .firstOrNull { it.session.toUri() == sessionUri } ?.let { baseSession -> - store.dispatch(WalletConnectAction.DisconnectSession(baseSession.session)) + store.dispatch(WalletConnectAction.DisconnectSession(baseSession.session.topic, baseSession.session)) + return } + wc2sessions.firstOrNull { it.sessionId == sessionUri }?.let { + store.dispatch(WalletConnectAction.DisconnectSession(sessionUri, null)) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt index 3d8c70a937..27b3abdb23 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt @@ -3,20 +3,15 @@ package com.tangem.tap.features.details.ui.walletconnect.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.store import com.tangem.wallet.R -import com.trustwallet.walletconnect.models.session.WCSession object BnbTransactionDialog { - fun create( - data: BinanceMessageData, - session: WCSession, - sessionId: Long, - dAppName: String, - context: Context, - ): AlertDialog { + fun create(preparedData: WcPreparedRequest.BnbTransaction, context: Context): AlertDialog { + val data = preparedData.preparedRequestData.data val message = when (data) { is BinanceMessageData.Trade -> data.tradeData.map { context.getString( @@ -37,7 +32,7 @@ object BnbTransactionDialog { val fullMessage = context.getString( R.string.wallet_connect_bnb_sign_message, - dAppName, + preparedData.preparedRequestData.dAppName, message, ) val positiveButtonTitle = context.getText(R.string.common_sign) @@ -48,14 +43,15 @@ object BnbTransactionDialog { setPositiveButton(positiveButtonTitle) { _, _ -> store.dispatch( WalletConnectAction.BinanceTransaction.Sign( - id = sessionId, + id = preparedData.requestId, data = data.data, - sessionData = session, + topic = preparedData.topic, ), ) + store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData)) } setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> - store.dispatch(WalletConnectAction.RejectRequest(session, sessionId)) + store.dispatch(WalletConnectAction.RejectRequest(preparedData.topic, preparedData.requestId)) } setOnDismissListener { store.dispatch(GlobalAction.HideDialog) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt index 5f6bf384ed..2e354e9f0a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt @@ -3,22 +3,24 @@ package com.tangem.tap.features.details.ui.walletconnect.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.store import com.tangem.wallet.R -import com.trustwallet.walletconnect.models.session.WCSession object PersonalSignDialog { - fun create(data: PersonalSignDialogData, context: Context): AlertDialog { + fun create(preparedData: WcPreparedRequest.EthSign, context: Context): AlertDialog { + val data = preparedData.preparedRequestData.dialogData val message = context.getString(R.string.wallet_connect_alert_sign_message, data.message) return AlertDialog.Builder(context).apply { setTitle(context.getString(R.string.wallet_connect_title)) setMessage(message) setPositiveButton(context.getText(R.string.common_sign)) { _, _ -> - store.dispatch(WalletConnectAction.SignMessage(data.session)) + store.dispatch(WalletConnectAction.SignMessage(data.topic)) + store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData)) } setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> - store.dispatch(WalletConnectAction.RejectRequest(data.session, data.id)) + store.dispatch(WalletConnectAction.RejectRequest(data.topic, data.id)) } setOnDismissListener { store.dispatch(GlobalAction.HideDialog) @@ -30,6 +32,6 @@ object PersonalSignDialog { data class PersonalSignDialogData( val dAppName: String, val message: String, - val session: WCSession, + val topic: String, val id: Long, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SessionProposalDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SessionProposalDialog.kt new file mode 100644 index 0000000000..ee1ab06996 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SessionProposalDialog.kt @@ -0,0 +1,41 @@ +package com.tangem.tap.features.details.ui.walletconnect.dialogs + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents +import com.tangem.tap.store +import com.tangem.wallet.R + +object SessionProposalDialog { + fun create( + sessionProposal: WalletConnectEvents.SessionProposal, + networks: String, + context: Context, + onApprove: () -> Unit, + onReject: () -> Unit, + ): AlertDialog { + val message = context.getString( + R.string.wallet_connect_request_session_start, + sessionProposal.name, + networks, + sessionProposal.url, + ) + return AlertDialog.Builder(context).apply { + setTitle(context.getString(R.string.wallet_connect_title)) + setMessage(message) + setPositiveButton(context.getText(R.string.common_start)) { _, _ -> + store.dispatch(GlobalAction.HideDialog) + onApprove() + } + setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> + store.dispatch(GlobalAction.HideDialog) + onReject() + } + setOnCancelListener { + store.dispatch(GlobalAction.HideDialog) + onReject() + } + }.create() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt index 4d502f7275..21ca09c15c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt @@ -3,14 +3,15 @@ package com.tangem.tap.features.details.ui.walletconnect.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.features.details.redux.walletconnect.WcTransactionType +import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType import com.tangem.tap.store import com.tangem.wallet.R -import com.trustwallet.walletconnect.models.session.WCSession object TransactionDialog { - fun create(data: TransactionRequestDialogData, context: Context): AlertDialog { + fun create(preparedData: WcPreparedRequest.EthTransaction, context: Context): AlertDialog { + val data = preparedData.preparedRequestData.dialogData val message = context.getString( R.string.wallet_connect_create_tx_message, data.dAppName, @@ -22,22 +23,23 @@ object TransactionDialog { ) val positiveButtonTitle = when (data.type) { - WcTransactionType.EthSignTransaction -> context.getText(R.string.common_sign) - WcTransactionType.EthSendTransaction -> context.getText(R.string.common_sign_and_send) + WcEthTransactionType.EthSignTransaction -> context.getText(R.string.common_sign) + WcEthTransactionType.EthSendTransaction -> context.getText(R.string.common_sign_and_send) } return AlertDialog.Builder(context).apply { setTitle(context.getString(R.string.wallet_connect_title)) setMessage(message) setPositiveButton(positiveButtonTitle) { _, _ -> if (data.isEnoughFundsToSend) { - store.dispatch(WalletConnectAction.SendTransaction(data.session)) + store.dispatch(WalletConnectAction.SendTransaction(data.topic)) + store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData)) } else { - store.dispatch(WalletConnectAction.RejectRequest(data.session, data.id)) + store.dispatch(WalletConnectAction.RejectRequest(data.topic, data.id)) store.dispatch(WalletConnectAction.NotEnoughFunds) } } setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> - store.dispatch(WalletConnectAction.RejectRequest(data.session, data.id)) + store.dispatch(WalletConnectAction.RejectRequest(data.topic, data.id)) } setOnDismissListener { store.dispatch(GlobalAction.HideDialog) @@ -54,7 +56,7 @@ data class TransactionRequestDialogData( val totalAmount: String, val balance: String, val isEnoughFundsToSend: Boolean, - val session: WCSession, + val topic: String, val id: Long, - val type: WcTransactionType, + val type: WcEthTransactionType, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SimpleOkDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SimpleOkDialog.kt index dc44583536..6c12a9f65c 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SimpleOkDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SimpleOkDialog.kt @@ -53,4 +53,17 @@ object SimpleOkDialog { ), context = context, ) + + fun create(dialog: AppDialog.OkCancelDialogRes, context: Context): AlertDialog { + return AlertDialog.Builder(context).apply { + setTitle(context.getString(dialog.headerId)) + setMessage(dialog.messageId) + setPositiveButton(dialog.okButton.title) { _, _ -> dialog.okButton.action?.invoke() } + setNegativeButton(dialog.cancelButton.title) { _, _ -> dialog.cancelButton.action?.invoke() } + setOnDismissListener { + store.dispatchDialogHide() + dialog.cancelButton.action?.invoke() + } + }.create() + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt index 99734202b0..5fab5032f5 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt @@ -3,6 +3,7 @@ package com.tangem.tap.proxy.redux import com.tangem.domain.card.ScanCardUseCase import com.tangem.features.tester.api.TesterRouter import com.tangem.features.wallet.navigation.WalletRouter +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import org.rekotlin.Action sealed interface DaggerGraphAction : Action { @@ -11,5 +12,6 @@ sealed interface DaggerGraphAction : Action { val testerRouter: TesterRouter, val scanCardUseCase: ScanCardUseCase, val walletRouter: WalletRouter, + val walletConnectInteractor: WalletConnectInteractor, ) : DaggerGraphAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt index a2c7994ce1..bdc38a631c 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt @@ -16,6 +16,7 @@ object DaggerGraphReducer { testerRouter = action.testerRouter, scanCardUseCase = action.scanCardUseCase, walletRouter = action.walletRouter, + walletConnectInteractor = action.walletConnectInteractor, ) } } diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index b59edc1928..a741e4dfd4 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -6,6 +6,9 @@ import com.tangem.domain.card.ScanCardUseCase import com.tangem.features.tester.api.TesterRouter import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.features.wallet.navigation.WalletRouter +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles import org.rekotlin.StateType @@ -17,6 +20,9 @@ data class DaggerGraphState( val scanCardUseCase: ScanCardUseCase? = null, val walletFeatureToggles: WalletFeatureToggles? = null, val walletRouter: WalletRouter? = null, + val walletConnectRepository: WalletConnectRepository? = null, + val walletConnectSessionsRepository: WalletConnectSessionsRepository? = null, + val walletConnectInteractor: WalletConnectInteractor? = null, ) : StateType { inline fun get(getDependency: DaggerGraphState.() -> T?): T { diff --git a/buildSrc/src/main/java/Dependency.kt b/buildSrc/src/main/java/Dependency.kt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt index 3f4ba17990..4f95438c33 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt @@ -110,6 +110,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { shopify = configValues.shopifyShop, zendesk = configValues.zendesk, swapReferrerAccount = configValues.swapReferrerAccount, + walletConnectProjectId = configValues.walletConnectProjectId, ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt index 9e49b887b7..24d8735376 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt @@ -18,4 +18,5 @@ data class Config( val shopify: ShopifyShop? = null, val zendesk: ZendeskConfig? = null, val swapReferrerAccount: SwapReferrerAccount? = null, + val walletConnectProjectId: String = "", ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt index 04b424a2b3..cd518d4c31 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt @@ -37,6 +37,7 @@ class ConfigValueModel( val amplitudeApiKey: String, val swapReferrerAccount: SwapReferrerAccount?, val kaspaSecondaryApiUrl: String, + val walletConnectProjectId: String, ) data class AppsFlyer( diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/FilesModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/FilesModule.kt new file mode 100644 index 0000000000..bbdb88274e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/FilesModule.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.files.AndroidFileReader +import com.tangem.datasource.files.FileReader +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface FilesModule { + + @Binds + @Singleton + fun bindFileReader(androidFileReader: AndroidFileReader): FileReader +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/files/AndroidFileReader.kt b/core/datasource/src/main/java/com/tangem/datasource/files/AndroidFileReader.kt new file mode 100644 index 0000000000..8ed8eb243b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/files/AndroidFileReader.kt @@ -0,0 +1,17 @@ +package com.tangem.datasource.files + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class AndroidFileReader @Inject constructor(@ApplicationContext private val context: Context) : FileReader { + override fun readFile(fileName: String): String { + return context.openFileInput(fileName).bufferedReader().readText() + } + + override fun rewriteFile(content: String, fileName: String) { + context.openFileOutput(fileName, Context.MODE_PRIVATE).use { + it.write(content.toByteArray(), 0, content.length) + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/files/FileReader.kt b/core/datasource/src/main/java/com/tangem/datasource/files/FileReader.kt new file mode 100644 index 0000000000..bdeb127b9f --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/files/FileReader.kt @@ -0,0 +1,6 @@ +package com.tangem.datasource.files + +interface FileReader { + fun readFile(fileName: String): String + fun rewriteFile(content: String, fileName: String) +} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 06b3dd6a6b..ab2fb12799 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -72,6 +72,8 @@ mviCore = "1.3.1" kotlinSerialization = "1.4.1" arrow = "1.2.0-RC" reactiveNetwork = "3.0.8" +walletConnectCore = "1.15.0" +walletConnectWeb3 = "1.8.0" # endregion Other libraries # region Tangem @@ -216,4 +218,6 @@ kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-j arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" } arrow-fx = { module = "io.arrow-kt:arrow-fx-coroutines", version.ref = "arrow" } reactive-network = { module = "com.github.pwittchen:reactivenetwork-rx2", version.ref = "reactiveNetwork" } +walletConnectCore = { module = "com.walletconnect:android-core", version.ref = "walletConnectCore" } +walletConnectWeb3 = { module = "com.walletconnect:web3wallet", version.ref = "walletConnectWeb3" } # endregion Other diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt index 7f12be0c15..39b4f01925 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt @@ -4,7 +4,7 @@ internal object AppConfig { const val packageName = "com.tangem.wallet" const val versionCode = 1 const val versionName = "1.0.0-SNAPSHOT" - const val minSdkVersion = 21 + const val minSdkVersion = 23 const val targetSdkVersion = 33 const val compileSdkVersion = 33 } \ No newline at end of file