Updated on 2026-08-14

This commit is contained in:
Tangem 2022-07-25 23:07:15 +04:00
commit 3f089ec4c1
14 changed files with 263 additions and 61 deletions

View file

@ -105,6 +105,12 @@ class DialogManager : StoreSubscriber<GlobalState> {
dAppName = state.dialog.dAppName,
context = context
)
is WalletConnectDialog.UnsupportedNetwork ->
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_scanner_error_unsupported_network,
context = context
)
is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context)
is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context)
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(context)

View file

@ -1,11 +1,13 @@
package com.tangem.tap.domain.walletconnect
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.toMap
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonParser
import com.google.gson.annotations.SerializedName
import com.tangem.blockchain.common.Blockchain
import com.trustwallet.walletconnect.JSONRPC_VERSION
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
@ -18,9 +20,11 @@ class EthSignHelper {
.create()
}
fun tryToParseEthTypedMessage(data: String): WCEthereumSignMessage? {
val request =
gson.fromJson<CustomJsonRpcRequests>(data)
fun parseCustomRequest(data: String): CustomJsonRpcRequest {
return gson.fromJson<CustomJsonRpcRequest>(data)
}
fun tryToParseEthTypedMessage(request: CustomJsonRpcRequest): WCEthereumSignMessage? {
return if (request.method == WCMethodExtended.ETH_SIGN_TYPE_DATA_V4) {
WCEthereumSignMessage(
listOf(
@ -53,12 +57,26 @@ class EthSignHelper {
}
}
data class CustomJsonRpcRequests(
data class CustomJsonRpcRequest(
val id: Long,
val jsonrpc: String = JSONRPC_VERSION,
val method: WCMethodExtended?,
val params: JsonArray
)
) {
fun blockchainFromChainId(): Blockchain? {
return try {
val hex = params[0].asJsonObject.toMap()[CHAIN_ID_KEY]?.asString ?: ""
Blockchain.fromChainId(Integer.decode(hex))
} catch (exception: Exception) {
null
}
}
companion object {
const val CHAIN_ID_KEY = "chainId"
}
}
enum class WCMethodExtended {
@SerializedName("wc_sessionRequest")
@ -95,5 +113,10 @@ enum class WCMethodExtended {
GET_ACCOUNTS,
@SerializedName("trust_signTransaction")
SIGN_TRANSACTION;
SIGN_TRANSACTION,
@SerializedName("wallet_switchEthereumChain")
SWITCH_CHAIN,
;
}

View file

@ -11,14 +11,20 @@ import com.tangem.tap.store
import com.tangem.tap.walletConnectRepository
import com.trustwallet.walletconnect.WCClient
import com.trustwallet.walletconnect.models.WCPeerMeta
import com.trustwallet.walletconnect.models.binance.*
import com.trustwallet.walletconnect.models.binance.WCBinanceCancelOrder
import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder
import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder
import com.trustwallet.walletconnect.models.binance.WCBinanceTxConfirmParam
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction
import com.trustwallet.walletconnect.models.session.WCSession
import com.trustwallet.walletconnect.models.session.WCSessionUpdate
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import okhttp3.*
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber
import java.util.*
@ -71,6 +77,22 @@ class WalletConnectManager {
}
}
fun updateBlockchain(session: WalletConnectSession) {
sessions[session.session]?.client?.updateSession(
accounts = listOfNotNull(session.getAddress()),
chainId = session.wallet.blockchain?.getChainId(),
approved = true
)
val updatedSession = sessions[session.session]?.copy(
wallet = session.wallet
)
if (updatedSession != null) {
sessions[session.session] = updatedSession
}
}
private fun setupConnectionTimeoutCheck(session: WCSession) {
scope.launch {
delay(20_000)
@ -409,23 +431,43 @@ class WalletConnectManager {
Timber.d("Custom Request")
Timber.d(data)
val message = EthSignHelper.tryToParseEthTypedMessage(data)
if (message != null) {
Timber.d("onEthSign_v4: $message")
store.state.globalState.analyticsHandlers?.logWcEvent(
Analytics.WcAnalyticsEvent.Action(
Analytics.WcAction.PersonalSign
val request = EthSignHelper.parseCustomRequest(data)
when (request.method) {
WCMethodExtended.ETH_SIGN_TYPE_DATA_V4 -> handleTypedDataV4(request, client, id)
WCMethodExtended.SWITCH_CHAIN -> {
val blockchain = request.blockchainFromChainId()
Timber.d("WC switch chainID\nNew Blockchain: $blockchain")
val session = sessions[client.session]?.toWalletConnectSession()
if (session != null) {
store.dispatchOnMain(WalletConnectAction.SwitchBlockchain(blockchain, session))
}
}
else -> {
Timber.d("WC: unrecognized custom request")
}
}
}
}
private fun handleTypedDataV4(request: CustomJsonRpcRequest, client: WCClient, id: Long) {
val message = EthSignHelper.tryToParseEthTypedMessage(request)
if (message != null) {
Timber.d("onEthSign_v4: $message")
store.state.globalState.analyticsHandlers?.logWcEvent(
Analytics.WcAnalyticsEvent.Action(
Analytics.WcAction.PersonalSign
)
)
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(
WalletConnectAction.HandlePersonalSignRequest(
message,
sessionData,
id
)
)
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(
WalletConnectAction.HandlePersonalSignRequest(
message,
sessionData,
id
)
)
}
}
}
}

View file

@ -0,0 +1,95 @@
package com.tangem.tap.domain.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.card.EllipticCurve
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.domain.extensions.getPrimaryCurve
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.details.redux.walletconnect.WalletForSession
import com.tangem.tap.features.wallet.redux.WalletState
class WcWalletManagerFactory(
private val factory: WalletManagerFactory,
private val currenciesRepository: CurrenciesRepository,
) {
suspend fun getWalletManager(
wallet: WalletForSession, blockchain: Blockchain, walletState: WalletState
): WalletManager? {
val blockchainToMake = if (blockchain == Blockchain.Ethereum && wallet.isTestNet) {
Blockchain.EthereumTestnet
} else {
blockchain
}
val blockchainNetwork = BlockchainNetwork(
blockchain = blockchainToMake,
derivationPath = wallet.derivationPath?.rawPath,
tokens = emptyList()
)
return if (walletState.cardId == wallet.cardId) {
walletState.getWalletManager(blockchainNetwork)
} else {
val blockchainNetworkWithTokens = currenciesRepository
.loadSavedCurrencies(
cardId = wallet.cardId,
isHdWalletSupported = wallet.derivationPath != null
).firstOrNull { it == blockchainNetwork }
if (blockchainNetworkWithTokens != null) {
factory.makeWalletManager(
cardId = wallet.cardId,
blockchain = blockchainToMake,
publicKey = Wallet.PublicKey(
wallet.walletPublicKey!!,
wallet.derivedPublicKey,
wallet.derivationPath
),
tokens = blockchainNetworkWithTokens.tokens,
curve = blockchainToMake.getPrimaryCurve() ?: EllipticCurve.Secp256k1
)
} else {
null
}
}
}
suspend fun getWalletManager(
scanResponse: ScanResponse, blockchain: Blockchain, walletState: WalletState
): WalletManager? {
val card = scanResponse.card
val blockchainToMake = if (blockchain == Blockchain.Ethereum && card.isTestCard) {
Blockchain.EthereumTestnet
} else {
blockchain
}
val blockchainNetwork = BlockchainNetwork(
blockchain = blockchainToMake,
card = card
)
return if (walletState.cardId == card.cardId) {
walletState.getWalletManager(blockchainNetwork)
} else {
if (currenciesRepository
.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
.contains(blockchainNetwork)
) {
factory.makeWalletManagerForApp(
scanResponse = scanResponse,
blockchainNetwork = blockchainNetwork
)
} else {
null
}
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.details.redux.walletconnect
import android.app.Activity
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.wallet.R
@ -47,6 +48,16 @@ sealed class WalletConnectAction : Action {
data class Success(val session: WalletConnectSession) : WalletConnectAction()
}
data class SwitchBlockchain(
val blockchain: Blockchain?,
val session: WalletConnectSession
) : WalletConnectAction()
data class UpdateBlockchain(
val updatedSession: WalletConnectSession
) : WalletConnectAction()
data class FailureEstablishingSession(val session: WCSession?) : WalletConnectAction()
data class SetSessionsRestored(val sessions: List<WalletConnectSession>) :

View file

@ -1,10 +1,8 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.guard
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.getFromClipboard
@ -14,11 +12,10 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
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.WcWalletManagerFactory
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.scope
import com.tangem.tap.store
@ -159,6 +156,45 @@ class WalletConnectMiddleware {
action.id, action.data, action.sessionData
)
}
is WalletConnectAction.SwitchBlockchain -> {
val blockchain = action.blockchain.guard {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork))
return
}
val factory = WcWalletManagerFactory(
factory = store.state.globalState.tapWalletManager.walletManagerFactory,
currenciesRepository = currenciesRepository
)
val walletState = store.state.walletState
scope.launch {
val walletManager = factory.getWalletManager(
wallet = action.session.wallet,
blockchain = blockchain,
walletState = walletState
).guard {
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.AddNetwork(blockchain.fullName)
)
)
return@launch
}
val updatedWallet = action.session.wallet.copy(
walletPublicKey = walletManager.wallet.publicKey.seedKey,
derivedPublicKey = walletManager.wallet.publicKey.derivedKey,
derivationPath = walletManager.wallet.publicKey.derivationPath,
blockchain = action.blockchain
)
val updatedSession = action.session.copy(wallet = updatedWallet)
store.dispatchOnMain(WalletConnectAction.UpdateBlockchain(updatedSession))
}
}
is WalletConnectAction.UpdateBlockchain -> {
walletConnectManager.updateBlockchain(action.updatedSession)
}
}
}
@ -189,8 +225,13 @@ class WalletConnectMiddleware {
return
}
val factory = WcWalletManagerFactory(
factory = store.state.globalState.tapWalletManager.walletManagerFactory,
currenciesRepository = currenciesRepository
)
val walletState = store.state.walletState
scope.launch {
val walletManager = getWalletManager(scanResponse, blockchain).guard {
val walletManager = factory.getWalletManager(scanResponse, blockchain, walletState).guard {
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(session.session))
store.dispatchOnMain(
GlobalAction.ShowDialog(
@ -232,37 +273,4 @@ class WalletConnectMiddleware {
}
}
private suspend fun getWalletManager(
scanResponse: ScanResponse, blockchain: Blockchain
): WalletManager? {
val card = scanResponse.card
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
val blockchainToMake = if (blockchain == Blockchain.Ethereum && card.isTestCard) {
Blockchain.EthereumTestnet
} else {
blockchain
}
val blockchainNetwork = BlockchainNetwork(
blockchain = blockchainToMake,
card = card
)
return if (store.state.globalState.scanResponse?.card?.cardId == card.cardId) {
store.state.walletState.getWalletManager(blockchainNetwork)
} else {
if (currenciesRepository
.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
.contains(blockchainNetwork)
) {
factory.makeWalletManagerForApp(
scanResponse = scanResponse,
blockchainNetwork = blockchainNetwork
)
} else {
null
}
}
}
}

View file

@ -34,6 +34,11 @@ class WalletConnectReducer {
is WalletConnectAction.RefuseOpeningSession -> state.copy(loading = false)
is WalletConnectAction.OpeningSessionTimeout -> state.copy(loading = false)
is WalletConnectAction.FailureEstablishingSession -> state.copy(loading = false)
is WalletConnectAction.UpdateBlockchain -> state.copy(
sessions = state.sessions
.filterNot { it.peerId == action.updatedSession.peerId }
+ action.updatedSession
)
else -> state
}

View file

@ -82,6 +82,7 @@ data class WalletForSession(
sealed class WalletConnectDialog : StateDialog {
data class ClipboardOrScanQr(val clipboardUri: String) : WalletConnectDialog()
object UnsupportedCard : WalletConnectDialog()
object UnsupportedNetwork : WalletConnectDialog()
data class AddNetwork(val network: String) : WalletConnectDialog()
object OpeningSessionRejected : WalletConnectDialog()
object SessionTimeout : WalletConnectDialog()

View file

@ -73,6 +73,9 @@ data class WalletState(
val walletManagers: List<WalletManager>
get() = wallets.mapNotNull { it.walletManager }
val cardId: String?
get() = wallets.firstOrNull()?.walletManager?.wallet?.cardId
fun getWalletManager(currency: Currency?): WalletManager? {
if (currency?.blockchain == null) return null
return getWalletStore(currency)?.walletManager

View file

@ -59,5 +59,6 @@
<string name="token_details_unable_hide_alert_message">The %s token is the main currency on the %s network and cannot be hidden as long as you have other tokens on this network in the list.</string>
<string name="wallet_connect_network_not_found_format">%s network not found. Please, add it first and try again.</string>
<string name="wallet_connect_scanner_error_unsupported_network">This network is not supported. Please select another network.</string>
<string name="wallet_currency_subtitle">%s network</string>
</resources>

View file

@ -59,5 +59,6 @@
<string name="token_details_unable_hide_alert_message">The %s token is the main currency on the %s network and cannot be hidden as long as you have other tokens on this network in the list.</string>
<string name="wallet_connect_network_not_found_format">%s network not found. Please, add it first and try again.</string>
<string name="wallet_connect_scanner_error_unsupported_network">This network is not supported. Please select another network.</string>
<string name="wallet_currency_subtitle">%s network</string>
</resources>

View file

@ -59,5 +59,7 @@
<string name="token_details_unable_hide_alert_message">The %s token is the main currency on the %s network and cannot be hidden as long as you have other tokens on this network in the list.</string>
<string name="wallet_connect_network_not_found_format">%s network not found. Please, add it first and try again.</string>
<string name="wallet_connect_scanner_error_unsupported_network">This network is not supported. Please select another network.</string>
<string name="wallet_currency_subtitle">%s network</string>
</resources>

View file

@ -65,6 +65,8 @@
<string name="token_details_unable_hide_alert_message">Токен %s является основной валютой в сети %s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети.</string>
<string name="wallet_connect_network_not_found_format">Сеть %s не найдена. Пожалуйста, добавьте её и попробуйте заново.</string>
<string name="wallet_connect_scanner_error_unsupported_network">Сеть не поддерживается. Пожалуйста, выберите другую сеть.</string>
<string name="wallet_currency_subtitle">Сеть %s</string>
<string name="main_no_backup_warning_title">Бэкап кошелька не был произведен</string>

View file

@ -64,6 +64,8 @@
<string name="token_details_unable_hide_alert_message">The %s token is the main currency on the %s network and cannot be hidden as long as you have other tokens on this network in the list.</string>
<string name="wallet_connect_network_not_found_format">%s network not found. Please, add it first and try again.</string>
<string name="wallet_connect_scanner_error_unsupported_network">This network is not supported. Please select another network.</string>
<string name="wallet_currency_subtitle">%s network</string>
<string name="main_no_backup_warning_title">Your wallet has not been backed up</string>