Updated on 2026-08-14

This commit is contained in:
Tangem 2023-03-02 00:22:28 +04:00
parent e77eff8935
commit 92064843db
67 changed files with 2232 additions and 337 deletions

View file

@ -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)

View file

@ -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">
<meta-data
android:name="com.google.android.gms.wallet.api.enabled"

@ -1 +1 @@
Subproject commit c714678d9c46dee1b15ef9e8d27aa23afa1dfd8d
Subproject commit 9eaedddd9bfe5a1f216887d31eda0fabeba1c11d

View file

@ -32,6 +32,7 @@ import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.features.welcome.redux.WelcomeAction
@ -91,6 +92,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var walletRouter: WalletRouter
@Inject
lateinit var walletConnectInteractor: WalletConnectInteractor
private var snackbar: Snackbar? = null
private val dialogManager = DialogManager()
private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind)
@ -119,12 +123,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
GooglePayService(createPaymentsClient(this), this),
),
)
store.dispatch(
DaggerGraphAction.SetActivityDependencies(
testerRouter = testerRouter,
scanCardUseCase = scanCardUseCase,
walletRouter = walletRouter,
walletConnectInteractor = walletConnectInteractor,
),
)
}

View file

@ -1,66 +1,60 @@
package com.tangem.tap
import android.app.Application
import android.content.Context
import android.content.pm.PackageManager
import coil.ImageLoader
import coil.ImageLoaderFactory
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.FeaturesLocalLoader
import com.tangem.datasource.config.models.Config
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.DomainLayer
import com.tangem.domain.common.LogConfig
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.tap.common.IntentHandler
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.common.analytics.topup.TopUpController
import com.tangem.tap.common.chat.ChatManager
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.shop.TangemShopService
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator
import com.tangem.tap.domain.totalBalance.di.provideDefaultImplementation
import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
import com.tangem.tap.domain.walletCurrencies.di.provideDefaultImplementation
import com.tangem.tap.domain.walletStores.WalletStoresManager
import com.tangem.tap.domain.walletStores.di.provideDefaultImplementation
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation
import android.app.*
import android.content.*
import android.content.pm.*
import coil.*
import com.tangem.*
import com.tangem.blockchain.common.*
import com.tangem.blockchain.network.*
import com.tangem.core.analytics.*
import com.tangem.core.featuretoggle.manager.*
import com.tangem.data.source.preferences.*
import com.tangem.datasource.api.common.*
import com.tangem.datasource.asset.*
import com.tangem.datasource.config.*
import com.tangem.datasource.config.models.*
import com.tangem.datasource.connection.*
import com.tangem.domain.*
import com.tangem.domain.common.*
import com.tangem.features.wallet.featuretoggles.*
import com.tangem.tap.common.*
import com.tangem.tap.common.analytics.*
import com.tangem.tap.common.analytics.api.*
import com.tangem.tap.common.analytics.handlers.amplitude.*
import com.tangem.tap.common.analytics.handlers.appsFlyer.*
import com.tangem.tap.common.analytics.handlers.firebase.*
import com.tangem.tap.common.analytics.topup.*
import com.tangem.tap.common.chat.*
import com.tangem.tap.common.feedback.*
import com.tangem.tap.common.images.*
import com.tangem.tap.common.log.*
import com.tangem.tap.common.redux.*
import com.tangem.tap.common.redux.global.*
import com.tangem.tap.common.shop.*
import com.tangem.tap.domain.configurable.warningMessage.*
import com.tangem.tap.domain.tokens.*
import com.tangem.tap.domain.totalBalance.*
import com.tangem.tap.domain.totalBalance.di.*
import com.tangem.tap.domain.walletCurrencies.*
import com.tangem.tap.domain.walletCurrencies.di.*
import com.tangem.tap.domain.walletStores.*
import com.tangem.tap.domain.walletStores.di.*
import com.tangem.tap.domain.walletStores.repository.*
import com.tangem.tap.domain.walletStores.repository.di.*
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.domain.walletconnect2.domain.*
import com.tangem.tap.features.customtoken.api.featuretoggles.*
import com.tangem.tap.proxy.*
import com.tangem.tap.proxy.redux.*
import com.tangem.wallet.BuildConfig
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.launch
import okhttp3.logging.HttpLoggingInterceptor
import org.rekotlin.Store
import timber.log.Timber
import javax.inject.Inject
import dagger.hilt.android.*
import kotlinx.coroutines.*
import okhttp3.logging.*
import org.rekotlin.*
import timber.log.*
import javax.inject.*
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository as WalletConnect2Repository
lateinit var store: Store<AppState>
@ -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() {

View file

@ -93,14 +93,11 @@ class DialogManager : StoreSubscriber<GlobalState> {
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<GlobalState> {
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)

View file

@ -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)
}
}

View file

@ -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

View file

@ -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(

View file

@ -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 {

View file

@ -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

View file

@ -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) {

View file

@ -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

View file

@ -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<WCSession, WalletConnectActiveData> = mutableMapOf()
private var sessions: MutableMap<Topic, WalletConnectActiveData> = 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?,

View file

@ -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 -> {

View file

@ -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<String> = listOf("dydx.exchange")
private val unsupportedDappsList: List<String> = 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
},
)
}

View file

@ -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<Blockchain>() // TODO: add supported networks
private const val EVM_NAMESPACE = "eip155"
private const val CHAIN_SEPARATOR = ":"

View file

@ -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<WcSessionForScreen>) {
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)
}
}

View file

@ -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<NetworkNamespace, List<Account>>? = null
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val _events: MutableSharedFlow<WalletConnectEvents> = MutableSharedFlow()
override val events: Flow<WalletConnectEvents> = _events
private val _activeSessions: MutableSharedFlow<List<WalletConnectSession>> = MutableSharedFlow()
override val activeSessions: Flow<List<WalletConnectSession>> = _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<NetworkNamespace, List<Account>>) {
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<String, Wallet.Model.Namespace.Proposal>,
userNamespaces: Map<NetworkNamespace, List<Account>>,
): Collection<String> {
val requiredChains = namespaces.values.flatMap { it.chains ?: emptyList() }
val userChains = userNamespaces.flatMap { it.value.map { account -> account.chainId } }
return requiredChains.subtract(userChains.toSet())
}
}

View file

@ -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<List<Session>> = moshi.adapter(
Types.newParameterizedType(List::class.java, Session::class.java),
)
override suspend fun loadSessions(userWallet: String): List<Session> {
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<Session>, 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()}"
}
}
}

View file

@ -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
}

View file

@ -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<WcSessionForScreen>)
fun onSessionRequest(request: WcPreparedRequest)
fun onUnsupportedRequest()
}

View file

@ -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<WalletConnectSession>,
relevantTopics: List<String>,
): List<WcSessionForScreen> {
return availableSessions.filter { relevantTopics.contains(it.topic) }
.map {
WcSessionForScreen(
description = it.name ?: "",
sessionId = it.topic,
)
}
}
private suspend fun getTopicsForUserWallet(
userWalletId: String,
repository: WalletConnectSessionsRepository,
): List<String> {
return repository.loadSessions(userWalletId).map { it.topic }
}
fun approveSessionProposal(accounts: List<Account>) {
val userNamespaces: Map<NetworkNamespace, List<Account>> = 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)
}
}

View file

@ -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<WalletConnectEvents>
val activeSessions: Flow<List<WalletConnectSession>>
fun init(projectId: String)
fun updateSessions()
fun pair(uri: String)
fun disconnect(topic: String)
fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>)
fun reject()
fun sendRequest(topic: String, id: Long, result: String)
fun rejectRequest(topic: String, id: Long)
}

View file

@ -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<Session>
suspend fun saveSession(userWallet: String, session: Session)
suspend fun removeSession(userWallet: String, topic: String)
}

View file

@ -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?
}

View file

@ -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<String>,
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<List<WcEthereumTransaction>>(
Types.newParameterizedType(List::class.java, WcEthereumTransaction::class.java),
).fromJsonFirstOrNull(params) ?: return customRequest
WcRequest.EthSignTransaction(data = deserializedParams)
}
WcJrpcMethods.ETH_SEND_TRANSACTION -> {
val deserializedParams = moshi.adapter<List<WcEthereumTransaction>>(
Types.newParameterizedType(List::class.java, WcEthereumTransaction::class.java),
).fromJsonFirstOrNull(params) ?: return customRequest
WcRequest.EthSendTransaction(data = deserializedParams)
}
WcJrpcMethods.ETH_SIGN -> {
val deserializedParams = moshi.adapter<List<String>>(
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<List<String>>(
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<List<WCBinanceTxConfirmParam>>(
Types.newParameterizedType(List::class.java, WCBinanceTxConfirmParam::class.java),
).fromJsonFirstOrNull(params) ?: return customRequest
WcRequest.BnbTxConfirm(data = deserializedParams)
}
WcJrpcMethods.SIGN_TRANSACTION -> {
val deserializedParams = moshi.adapter<List<WCSignTransaction>>(
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<List<WcBinanceCancelOrder>>(
Types.newParameterizedType(List::class.java, WcBinanceCancelOrder::class.java),
).fromJsonFirstOrNull(params)
if (cancelOrder != null) return WcRequest.BnbCancel(cancelOrder)
val tradeOrder = moshi.adapter<List<WcBinanceTradeOrder>>(
Types.newParameterizedType(List::class.java, WcBinanceTradeOrder::class.java),
).fromJsonFirstOrNull(params)
if (tradeOrder != null) return WcRequest.BnbTrade(tradeOrder)
val transferOrder = moshi.adapter<List<WcBinanceTransferOrder>>(
Types.newParameterizedType(List::class.java, WcBinanceTransferOrder::class.java),
).fromJsonFirstOrNull(params)
if (transferOrder != null) return WcRequest.BnbTransfer(transferOrder)
return null
}
private fun <T> JsonAdapter<T>.fromJsonOrNull(data: String): T? {
return try {
this.fromJson(data)
} catch (e: Exception) {
Timber.e(e.message)
null
}
}
private fun <T> JsonAdapter<List<T>>.fromJsonFirstOrNull(data: String): T? {
return try {
this.fromJson(data)?.firstOrNull()
} catch (e: Exception) {
Timber.e(e.message)
null
}
}
}

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -0,0 +1,3 @@
package com.tangem.tap.domain.walletconnect2.domain.models
data class Account(val chainId: String, val walletAddress: String, val derivationPath: String?)

View file

@ -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,
)

View file

@ -0,0 +1,6 @@
package com.tangem.tap.domain.walletconnect2.domain.models
data class ChainWithDerivation(
val chain: String,
val derivationPath: String?,
)

View file

@ -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,
)

View file

@ -0,0 +1,4 @@
package com.tangem.tap.domain.walletconnect2.domain.models
@JvmInline
value class NetworkNamespace(val value: String)

View file

@ -0,0 +1,15 @@
package com.tangem.tap.domain.walletconnect2.domain.models
data class Session(
val topic: String,
val accounts: List<Account>,
) {
companion object {
fun fromAccounts(accounts: List<Account>, topic: String): Session {
return Session(
topic = topic,
accounts = accounts,
)
}
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.tap.domain.walletconnect2.domain.models
sealed class WalletConnectError : Exception() {
data class ApprovalErrorMissingNetworks(val missingChains: List<String>) : WalletConnectError()
data class ApprovalErrorAddNetwork(val networks: List<String>) : WalletConnectError()
object ApprovalErrorUnsupportedNetwork : WalletConnectError()
data class ExternalApprovalError(override val message: String?) : WalletConnectError()
object WrongUserWallet : WalletConnectError()
object UnsupportedMethod : WalletConnectError()
}

View file

@ -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<URI>,
val chainIds: List<String>,
) : WalletConnectEvents
data class SessionApprovalError(val error: WalletConnectError) : WalletConnectEvents
data class SessionApprovalSuccess(val topic: String, val accounts: List<Account>) : 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
}

View file

@ -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?,
)

View file

@ -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<Message>,
) : WcBinanceOrder<WcBinanceCancelOrder.Message>(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,
)
}

View file

@ -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<T>(
val accountNumber: String,
val chainId: String,
val data: String?,
val memo: String?,
val sequence: String,
val source: String,
val msgs: List<T>,
) : WcRequestData
data class WCBinanceTxConfirmParam(
val ok: Boolean,
val errorMsg: String?,
) : WcRequestData

View file

@ -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<Message>,
) : WcBinanceOrder<WcBinanceTradeOrder.Message>(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<WcBinanceTradeOrder.Message> {
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
}

View file

@ -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
}
}
}
}

View file

@ -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<Message>,
) : WcBinanceOrder<WcBinanceTransferOrder.Message>(accountNumber, chainId, data, memo, sequence, source, msgs) {
enum class MessageKey(val key: String) {
INPUTS("inputs"),
OUTPUTS("outputs"),
}
data class Message(
val inputs: List<Item>,
val outputs: List<Item>,
) {
data class Item(
val address: String,
val coins: List<Coin>,
) {
data class Coin(
val amount: Long,
val denom: String,
)
}
}
}

View file

@ -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<WalletConnectSession>) :
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<WcSessionForScreen>) : WalletConnectAction()
data class ShowSessionRequest(val sessionRequest: WcPreparedRequest) : WalletConnectAction()
object RejectUnsupportedRequest : WalletConnectAction()
data class PerformRequestedAction(val sessionRequest: WcPreparedRequest) : WalletConnectAction()
//endregion WalletConnect 2.0
}

View file

@ -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<AppState> = { 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))
}
}
}

View file

@ -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
}
}

View file

@ -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<WalletConnectSession> = listOf(),
val wc2Sessions: List<WcSessionForScreen> = listOf(),
val newSessionData: NewWcSessionData? = null,
)
@ -95,43 +100,47 @@ sealed class WalletConnectDialog : StateDialog {
val networks: List<Blockchain>,
) : 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<Blockchain>,
) : 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(

View file

@ -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 = "",

View file

@ -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<WcSessionForScreen>,
val sessions: ImmutableList<WcSessionForScreen>,
val isLoading: Boolean = false,
val onRemoveSession: (String) -> Unit = {},
val onAddSession: (String?) -> Unit = {},

View file

@ -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<AppState>) {
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<WalletConnectSession>) {
private fun onRemoveSession(
sessionUri: String,
sessions: List<WalletConnectSession>,
wc2sessions: List<WcSessionForScreen>,
) {
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))
}
}
}

View file

@ -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)

View file

@ -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,
)

View file

@ -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()
}
}

View file

@ -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,
)

View file

@ -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()
}
}

View file

@ -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
}

View file

@ -16,6 +16,7 @@ object DaggerGraphReducer {
testerRouter = action.testerRouter,
scanCardUseCase = action.scanCardUseCase,
walletRouter = action.walletRouter,
walletConnectInteractor = action.walletConnectInteractor,
)
}
}

View file

@ -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 <reified T> get(getDependency: DaggerGraphState.() -> T?): T {

View file

View file

@ -110,6 +110,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
shopify = configValues.shopifyShop,
zendesk = configValues.zendesk,
swapReferrerAccount = configValues.swapReferrerAccount,
walletConnectProjectId = configValues.walletConnectProjectId,
)
}
}

View file

@ -18,4 +18,5 @@ data class Config(
val shopify: ShopifyShop? = null,
val zendesk: ZendeskConfig? = null,
val swapReferrerAccount: SwapReferrerAccount? = null,
val walletConnectProjectId: String = "",
)

View file

@ -37,6 +37,7 @@ class ConfigValueModel(
val amplitudeApiKey: String,
val swapReferrerAccount: SwapReferrerAccount?,
val kaspaSecondaryApiUrl: String,
val walletConnectProjectId: String,
)
data class AppsFlyer(

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.datasource.files
interface FileReader {
fun readFile(fileName: String): String
fun rewriteFile(content: String, fileName: String)
}

View file

@ -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

View file

@ -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
}