Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-05 18:54:08 +04:00
parent fd7bc522f2
commit 194e750c93
74 changed files with 52 additions and 4627 deletions

View file

@ -35,6 +35,7 @@ import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
@ -50,7 +51,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
@EntryPoint
@InstallIn(SingletonComponent::class)
@ -71,8 +71,6 @@ interface ApplicationEntryPoint {
fun getCardScanningFeatureToggles(): CardScanningFeatureToggles
fun getWalletConnect2Repository(): WalletConnect2Repository
fun getScanCardProcessor(): ScanCardProcessor
fun getAppCurrencyRepository(): AppCurrencyRepository
@ -151,4 +149,6 @@ interface ApplicationEntryPoint {
fun getTangemHotSdk(): TangemHotSdk
fun getHotWalletFeatureToggles(): HotWalletFeatureToggles
fun getWcInitializeUseCase(): WcInitializeUseCase
}

View file

@ -40,6 +40,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.settings.SetGooglePayAvailabilityUseCase
import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase
@ -49,12 +50,10 @@ import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.features.tester.api.TesterMenuLauncher
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.google.GoogleServicesHelper
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder
@ -64,11 +63,9 @@ import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.features.intentHandler.IntentProcessor
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.features.main.MainViewModel
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.tap.routing.component.RoutingComponent
@ -116,9 +113,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
lateinit var scanCardUseCase: ScanCardUseCase
@Inject
lateinit var walletConnectInteractor: WalletConnectInteractor
@Inject
lateinit var settingsRepository: SettingsRepository
@ -171,9 +165,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
internal lateinit var deeplinkFactory: DeepLinkFactory
@Inject
internal lateinit var walletConnectFeatureToggles: WalletConnectFeatureToggles
@Inject
internal lateinit var urlOpener: UrlOpener
@ -183,9 +174,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
internal lateinit var intentProcessor: IntentProcessor
@Inject
internal lateinit var walletConnectLinkIntentHandler: WalletConnectLinkIntentHandler
@Inject
internal lateinit var onPushClickedIntentHandler: OnPushClickedIntentHandler
@ -201,7 +189,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
internal lateinit var tangemPayFeatureToggles: TangemPayFeatureToggles
internal val viewModel: MainViewModel by viewModels()
private val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
@ -293,7 +281,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
store.dispatch(
DaggerGraphAction.SetActivityDependencies(
scanCardUseCase = scanCardUseCase,
walletConnectInteractor = walletConnectInteractor,
cardSdkConfigRepository = cardSdkConfigRepository,
),
)
@ -369,10 +356,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
private fun initIntentHandlers() {
intentProcessor.addHandler(onPushClickedIntentHandler)
if (!walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
intentProcessor.addHandler(walletConnectLinkIntentHandler)
}
}
private fun updateAppTheme(appThemeMode: AppThemeMode) {

View file

@ -75,10 +75,12 @@ import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.wallet.BuildConfig
import dagger.hilt.EntryPoints
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.rekotlin.Store
import timber.log.Timber
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
lateinit var store: Store<AppState>
@ -112,9 +114,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
private val cardScanningFeatureToggles: CardScanningFeatureToggles
get() = entryPoint.getCardScanningFeatureToggles()
private val walletConnect2Repository: WalletConnect2Repository
get() = entryPoint.getWalletConnect2Repository()
private val scanCardProcessor: ScanCardProcessor
get() = entryPoint.getScanCardProcessor()
@ -237,6 +236,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
private val hotWalletFeatureToggles
get() = entryPoint.getHotWalletFeatureToggles()
private val wcInitializeUseCase
get() = entryPoint.getWcInitializeUseCase()
// endregion
private val appScope = MainScope()
@ -331,7 +333,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
)
appStateHolder.mainStore = store
walletConnect2Repository.init(
wcInitializeUseCase.init(
projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId,
)
}
@ -344,7 +347,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
daggerGraphState = DaggerGraphState(
networkConnectionManager = networkConnectionManager,
cardScanningFeatureToggles = cardScanningFeatureToggles,
walletConnectRepository = walletConnect2Repository,
scanCardProcessor = scanCardProcessor,
appCurrencyRepository = appCurrencyRepository,
walletManagersFacade = walletManagersFacade,

View file

@ -5,9 +5,10 @@ import android.content.Context
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.ui.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.*
import com.tangem.tap.common.ui.ScanFailsDialog
import com.tangem.tap.common.ui.SimpleAlertDialog
import com.tangem.tap.common.ui.SimpleCancelableAlertDialog
import com.tangem.tap.common.ui.SimpleOkDialog
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog
@ -59,88 +60,6 @@ class DialogManager : StoreSubscriber<GlobalState> {
context = context,
)
is OnboardingDialog.WalletActivationError -> WalletActivationErrorDialog.create(context, state.dialog)
is WalletConnectDialog.UnsupportedCard ->
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_scanner_error_not_valid_card,
context = context,
)
is WalletConnectDialog.AddNetwork -> {
val message = context.getString(
R.string.wallet_connect_error_missing_blockchains,
) + state.dialog.networks.joinToString()
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
message = message,
context = context,
)
}
is WalletConnectDialog.OpeningSessionRejected -> {
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_same_wcuri,
context = context,
)
}
is WalletConnectDialog.SessionTimeout -> {
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_error_timeout,
context = context,
)
}
is WalletConnectDialog.RequestTransaction -> TransactionDialog.create(state.dialog.data, context)
is WalletConnectDialog.PersonalSign -> PersonalSignDialog.create(state.dialog.data, context)
is WalletConnectDialog.BnbTransactionDialog ->
BnbTransactionDialog.create(
preparedData = state.dialog.data,
context = context,
)
is WalletConnectDialog.UnsupportedNetwork -> {
val warning = if (state.dialog.networks.isNullOrEmpty()) {
context.getString(R.string.wallet_connect_scanner_error_unsupported_network)
} else {
context.getString(R.string.wallet_connect_error_unsupported_blockchains) +
state.dialog.networks.joinToString()
}
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
message = warning,
context = context,
)
}
is WalletConnectDialog.UnsupportedDapp -> SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_error_unsupported_dapp,
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 WalletConnectDialog.SignTransactionDialog -> SignTransactionDialog.create(
preparedData = state.dialog.data,
context = context,
)
is WalletConnectDialog.SignTransactionsDialog -> SignTransactionsDialog.create(
preparedData = state.dialog.data,
context = context,
)
is WalletConnectDialog.PairConnectErrorDialog -> SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
message = state.dialog.error.message,
context = context,
)
is WalletConnectDialog.UnsupportedWcVersion -> SimpleAlertDialog.create(
titleRes = R.string.common_error,
messageRes = R.string.unsupported_wc_version,
context = context,
)
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(
context = context,
scanResponse = state.dialog.scanResponse,

View file

@ -1,120 +0,0 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType
/**
[REDACTED_AUTHOR]
*/
internal sealed class WalletConnect(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Wallet Connect", event, params) {
class ScreenOpened : WalletConnect(event = "WC Screen Opened")
class NewSessionInitiated(source: SourceType) : WalletConnect(
event = "Session Initiated",
params = mapOf(
AnalyticsParam.SOURCE to when (source) {
SourceType.QR -> "QR"
SourceType.DEEPLINK -> "DeepLink"
SourceType.CLIPBOARD -> "Clipboard"
SourceType.ETC -> "etc"
},
),
)
data object SessionFailed : WalletConnect(
event = "Session Failed",
)
class DAppConnectionRequested(
blockchainNames: List<String>,
) : WalletConnect(
event = "dApp Connection Requested",
params = mapOf(
AnalyticsParam.NETWORKS to blockchainNames.joinToString(","),
),
)
class DAppConnected(dAppName: String, dAppUrl: String, blockchainNames: List<String>) : WalletConnect(
event = "dApp Connected",
params = mapOf(
AnalyticsParam.DAPP_NAME to dAppName,
AnalyticsParam.DAPP_URL to dAppUrl,
AnalyticsParam.BLOCKCHAIN to blockchainNames.joinToString(","),
),
)
class DAppConnectionFailed(dAppName: String, dAppUrl: String, blockchainNames: List<String>) : WalletConnect(
event = "dApp Connection Failed",
params = mapOf(
AnalyticsParam.DAPP_NAME to dAppName,
AnalyticsParam.DAPP_URL to dAppUrl,
AnalyticsParam.BLOCKCHAIN to blockchainNames.joinToString(","),
),
)
class SessionDisconnected(dAppName: String, dAppUrl: String) : WalletConnect(
event = "dApp Disconnected",
params = mapOf(
AnalyticsParam.DAPP_NAME to dAppName,
AnalyticsParam.DAPP_URL to dAppUrl,
),
)
class SignatureRequestHandled(
params: RequestHandledParams,
) : WalletConnect(
event = "Signature Request Handled",
params = params.toParamsMap(),
)
class SignatureRequestReceived(
params: RequestHandledParams,
) : WalletConnect(
event = "Signature Request Received",
params = params.toParamsMap(),
)
class SignatureRequestFailed(
params: RequestHandledParams,
) : WalletConnect(
event = "Signature Request Failed",
params = params.toParamsMap(),
)
data class RequestHandledParams(
val dAppName: String,
val dAppUrl: String,
val methodName: String,
val blockchain: String,
val errorCode: String? = null,
val errorDescription: String? = null,
) {
fun toParamsMap(): Map<String, String> {
val validation = if (errorCode == null) Validation.SUCCESS.param else Validation.FAIL.param
val code = errorCode ?: SUCCESS_CODE
return buildMap {
put(AnalyticsParam.DAPP_NAME, dAppName)
put(AnalyticsParam.DAPP_URL, dAppUrl)
put(AnalyticsParam.METHOD_NAME, methodName)
put(AnalyticsParam.BLOCKCHAIN, blockchain)
put(AnalyticsParam.VALIDATION, validation)
put(AnalyticsParam.ERROR_CODE, code)
if (errorDescription != null) {
put(AnalyticsParam.ERROR_DESCRIPTION, errorDescription)
}
}
}
}
enum class Validation(val param: String) {
SUCCESS("Success"),
FAIL("Fail"),
}
private companion object {
const val SUCCESS_CODE = "0"
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.global.globalReducer
import com.tangem.tap.features.details.redux.DetailsReducer
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.redux.DaggerGraphReducer
import org.rekotlin.Action
@ -14,7 +13,6 @@ fun appReducer(action: Action, state: AppState?): AppState {
return AppState(
globalState = globalReducer(action, state),
detailsState = DetailsReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
welcomeState = WelcomeReducer.reduce(action, state),
daggerGraphState = DaggerGraphReducer.reduce(action, state),
)

View file

@ -5,8 +5,6 @@ import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.legacy.LegacyMiddleware
import com.tangem.tap.features.details.redux.DetailsMiddleware
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
@ -19,7 +17,6 @@ import org.rekotlin.StateType
data class AppState(
val globalState: GlobalState = GlobalState(),
val detailsState: DetailsState = DetailsState(),
val walletConnectState: WalletConnectState = WalletConnectState(),
val welcomeState: WelcomeState = WelcomeState(),
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
) : StateType {
@ -30,7 +27,6 @@ data class AppState(
logMiddleware,
GlobalMiddleware.handler,
DetailsMiddleware().detailsMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,
WelcomeMiddleware().middleware,
LockUserWalletsTimerMiddleware().middleware,

View file

@ -4,7 +4,6 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.tap.features.intentHandler.IntentProcessor
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -19,10 +18,6 @@ internal object IntentHandlingModule {
@Singleton
fun provideBackgroundScanIntentHandler(): BackgroundScanIntentHandler = BackgroundScanIntentHandler()
@Provides
@Singleton
fun provideWalletConnectLinkIntentHandler(): WalletConnectLinkIntentHandler = WalletConnectLinkIntentHandler()
@Provides
@Singleton
fun provideOnPushClickedIntentHandler(analyticsEventHandler: AnalyticsEventHandler): OnPushClickedIntentHandler =

View file

@ -1,77 +0,0 @@
package com.tangem.tap.domain.walletconnect
import com.github.salomonbrys.kotson.registerTypeAdapter
import com.google.gson.GsonBuilder
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.calculateSha256
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.tangem.utils.extensions.stripZeroPlainString
import timber.log.Timber
internal object BnbHelper {
fun createMessageData(order: WcBinanceTransferOrder): BinanceMessageData.Transfer {
val input = order.msgs.first().inputs.first()
val output = order.msgs.first().inputs.first()
val currency =
input.coins.firstOrNull()?.denom ?: Blockchain.Binance
val amount = input.coins
.mapNotNull { if (it.denom == currency) it.amount else null }
.sum()
.toBigDecimal()
.movePointLeft(Blockchain.Binance.decimals())
.stripZeroPlainString()
val gson = GsonBuilder()
.registerTypeAdapter(tradeOrderSerializer)
.serializeNulls()
.create()
return BinanceMessageData.Transfer(
outputAddress = output.address,
amount = amount,
address = input.address,
data = gson.toJson(order).toByteArray().calculateSha256(),
)
}
fun createMessageData(order: WcBinanceTradeOrder): BinanceMessageData.Trade {
val address = order.msgs.first().sender
val tradeData = order.msgs.map {
val price = it.price.toBigDecimal()
.movePointLeft(Blockchain.Binance.decimals())
.stripTrailingZeros()
val quantity = it.quantity.toBigDecimal()
.movePointLeft(Blockchain.Binance.decimals())
.stripTrailingZeros()
val amount = price * quantity
val symbol = it.symbol.substringBefore("-")
TradeData(
price = "$price ${Blockchain.Binance.currency}",
quantity = "$quantity $symbol",
amount = "$amount ${Blockchain.Binance.currency}",
symbol = symbol,
)
}
val gson = GsonBuilder()
.registerTypeAdapter(tradeOrderSerializer)
.serializeNulls()
.create()
val serialized = gson.toJson(order)
Timber.d(serialized)
return BinanceMessageData.Trade(
tradeData = tradeData,
address = address,
data = serialized.toByteArray().calculateSha256(),
)
}
}

View file

@ -1,31 +0,0 @@
package com.tangem.tap.domain.walletconnect
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonParser
object EthSignHelper {
private val gson: Gson by lazy {
GsonBuilder()
.setPrettyPrinting()
.serializeNulls()
.create()
}
fun tryToParseEthTypedMessageString(message: String): String? {
return try {
val messageString = message
.replace("\\", "")
.removePrefix("\"")
.removeSuffix("\"")
val messageJson = JsonParser().parse(messageString)
val filteredMap = gson.fromJson<Map<*, *>>(messageJson)
.filterKeys { it == "domain" || it == "message" }
gson.toJson(filteredMap)
} catch (exception: Exception) {
null
}
}
}

View file

@ -1,529 +0,0 @@
package com.tangem.tap.domain.walletconnect
import com.tangem.Message
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.common.*
import com.tangem.blockchain.common.smartcontract.CompiledSmartContractCallData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.*
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString
import com.tangem.data.walletconnect.network.ethereum.LegacySdkHelper
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.domain.walletconnect2.domain.TransactionType
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction
import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage
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.demo.isDemoCard
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.ui.walletconnect.dialogs.PersonalSignDialogData
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import org.json.JSONArray
import org.json.JSONObject
import timber.log.Timber
import java.math.BigDecimal
@Suppress("LargeClass")
class WalletConnectSdkHelper {
private val userWalletsListManager by lazy {
store.inject(DaggerGraphState::generalUserWalletsListManager)
}
private val userWalletsListRepository by lazy {
store.inject(DaggerGraphState::userWalletsListRepository)
}
private val hotWalletFeatureToggles by lazy {
store.inject(DaggerGraphState::hotWalletFeatureToggles)
}
@Suppress("MagicNumber")
suspend fun prepareTransactionData(data: EthTransactionData): WcTransactionData {
val transaction = data.transaction
val blockchain = requireNotNull(Blockchain.fromNetworkId(data.networkId)) { "Blockchain not found" }
val walletManager = requireNotNull(getWalletManager(blockchain, data.rawDerivationPath)) {
"WalletManager not found"
}
walletManager.safeUpdate(isDemoCard())
val wallet = walletManager.wallet
val balance = requireNotNull(wallet.amounts[AmountType.Coin]?.value) {
"Coin balance not found"
}
val decimals = wallet.blockchain.decimals()
val value = (transaction.value ?: "0")
.hexToBigDecimal()
.movePointLeft(decimals)
requireNotNull(value) {
"Transaction amount is null"
}
// TODO move fee calculation to SDK getFee() [REDACTED_JIRA]
val gasLimit = getGasLimitFromTx(value, walletManager, transaction, blockchain)
val gasPrice = getGasPrice(walletManager, transaction)
val feeDecimal = (gasLimit * gasPrice).movePointLeft(decimals)
val total = value + feeDecimal
val feeAmount = Amount(feeDecimal, wallet.blockchain)
val destinationAddress = requireNotNull(transaction.to) { "Destination address is null" }
val fee = if (blockchain.isEvm()) {
// TODO [REDACTED_JIRA]
// workaround for Mantle, remove after [REDACTED_JIRA]
val patchedAmount = if (blockchain == Blockchain.Mantle) {
feeAmount.copy(value = feeAmount.value?.multiply(MANTLE_FEE_ESTIMATE_MULTIPLIER))
} else {
feeAmount
}
Fee.Ethereum.Legacy(patchedAmount, gasLimit.toBigInteger(), gasPrice.toBigInteger())
} else {
Fee.Common(feeAmount)
}
val transactionData = TransactionData.Uncompiled(
amount = Amount(value, wallet.blockchain),
fee = fee,
sourceAddress = transaction.from,
destinationAddress = destinationAddress,
extras = EthereumTransactionExtras(
callData = CompiledSmartContractCallData(transaction.data.removePrefix(HEX_PREFIX).hexToBytes()),
gasLimit = gasLimit.toBigInteger(),
nonce = transaction.nonce?.hexToBigDecimal()?.toBigInteger(),
),
)
val dialogData = TransactionRequestDialogData(
dAppName = data.metaName,
dAppUrl = data.metaUrl,
amount = value.toFormattedString(decimals),
feeAmount = feeDecimal.toFormattedString(decimals),
totalAmount = total.toFormattedString(decimals),
balance = balance.toFormattedString(decimals),
isEnoughFundsToSend = balance - total >= BigDecimal.ZERO,
topic = data.topic,
id = data.id,
type = data.type,
)
return WcTransactionData(
type = data.type,
transaction = transactionData,
topic = data.topic,
id = data.id,
walletManager = walletManager,
dialogData = dialogData,
)
}
suspend fun isDemoCard(): Boolean {
val userWallet = getSelectedWallet() ?: return false
return userWallet is UserWallet.Cold && userWallet.scanResponse.isDemoCard()
}
private suspend fun getWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager? {
val userWallet = getSelectedWallet() ?: return null
val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade)
return walletManagerFacade.getOrCreateWalletManager(
userWalletId = userWallet.walletId,
blockchain = blockchain,
derivationPath = derivationPath,
)
}
suspend fun completeTransaction(data: WcTransactionData, cardId: String?): String? {
return when (data.type) {
WcEthTransactionType.EthSendTransaction -> sendTransaction(data, cardId)
WcEthTransactionType.EthSignTransaction -> signTransaction(data, cardId)
}
}
private suspend fun getGasPrice(walletManager: WalletManager, transaction: WcEthereumTransaction): BigDecimal {
val txGasPrice = transaction.gasPrice?.hexToBigDecimal()
if (txGasPrice != null) {
return txGasPrice
}
return when (val result = (walletManager as? EthereumGasLoader)?.getGasPrice()) {
is Result.Success -> result.data.toBigDecimal()
is Result.Failure -> {
(result.error as? Throwable)?.let { Timber.e(it, "getGasPrice failed") }
error("Unable to get gas price: ${result.error}")
}
null -> error("Gas price is null")
}
}
private suspend fun getGasLimitFromTx(
value: BigDecimal,
walletManager: WalletManager,
transaction: WcEthereumTransaction,
blockchain: Blockchain,
): BigDecimal {
return transaction.gas?.hexToBigDecimal()
?: transaction.gasLimit?.hexToBigDecimal()
?: getGasLimitFromBlockchain(
value = value,
walletManager = walletManager,
transaction = transaction,
).increaseForMantleIfNeeded(blockchain)
}
private suspend fun getGasLimitFromBlockchain(
value: BigDecimal,
walletManager: WalletManager,
transaction: WcEthereumTransaction,
): BigDecimal {
val gasLimitResult = (walletManager as? EthereumGasLoader)?.getGasLimit(
amount = Amount(value, walletManager.wallet.blockchain),
destination = transaction.to ?: "",
callData = CompiledSmartContractCallData(transaction.data.hexToBytes()),
)
return when (gasLimitResult) {
is Result.Success -> gasLimitResult.data.toBigDecimal().multiply(BigDecimal("1.2"))
is Result.Failure -> {
(gasLimitResult.error as? Throwable)?.let { Timber.e(it, "getGasLimit failed") }
DEFAULT_MAX_GASLIMIT.toBigDecimal() // Set high gasLimit if not provided
}
else -> DEFAULT_MAX_GASLIMIT.toBigDecimal() // Set high gasLimit if not provided
}
}
// TODO Workaround for Mantle. Remove after [REDACTED_JIRA]
private fun BigDecimal.increaseForMantleIfNeeded(blockchain: Blockchain): BigDecimal {
return if (blockchain == Blockchain.Mantle) {
this.multiply(MANTLE_FEE_ESTIMATE_MULTIPLIER)
} else {
this
}
}
private suspend fun sendTransaction(data: WcTransactionData, cardId: String?): String? {
val sdk = store.inject(DaggerGraphState::cardSdkConfigRepository).sdk
val result = (data.walletManager as TransactionSender).send(
transactionData = data.transaction,
signer = store.inject(DaggerGraphState::transactionSignerFactory).createTransactionSigner(
cardId = cardId,
sdk = sdk,
twinKey = null, // use null here because twin doesn't support WC
),
)
return when (result) {
is Result.Success -> {
val hash = result.data.hash
if (hash.startsWith(HEX_PREFIX)) {
hash
} else {
HEX_PREFIX + hash
}
}
is Result.Failure -> {
Timber.e(result.error as BlockchainSdkError)
null
}
}
}
private suspend fun signTransaction(data: WcTransactionData, cardId: String?): String? {
val dataToSign = EthereumUtils.buildTransactionToSign(
transactionData = data.transaction,
blockchain = data.walletManager.wallet.blockchain,
)
val command = SignHashCommand(
hash = dataToSign.hash,
walletPublicKey = data.walletManager.wallet.publicKey.seedKey,
derivationPath = data.walletManager.wallet.publicKey.derivationPath,
)
return when (
val result = tangemSdkManager.runTaskAsync(
runnable = command,
initialMessage = Message(),
cardId = cardId,
preflightReadFilter = null,
)
) {
is CompletionResult.Success -> {
val hash = EthereumUtils.prepareTransactionToSend(
signature = result.data.signature,
transactionToSign = dataToSign,
walletPublicKey = data.walletManager.wallet.publicKey,
blockchain = data.walletManager.wallet.blockchain,
).toHexString()
if (hash.startsWith(HEX_PREFIX)) {
hash
} else {
HEX_PREFIX + hash
}
}
is CompletionResult.Failure -> {
Timber.e(result.error.customMessage)
null
}
}
}
fun prepareBnbTradeOrder(data: WcBinanceTradeOrder): BinanceMessageData.Trade {
return BnbHelper.createMessageData(data)
}
fun prepareBnbTransferOrder(data: WcBinanceTransferOrder): BinanceMessageData.Transfer {
return BnbHelper.createMessageData(data)
}
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 = wallet.publicKey.seedKey,
derivationPath = wallet.publicKey.derivationPath,
)
return when (
val result = tangemSdkManager.runTaskAsync(
runnable = command,
initialMessage = Message(),
cardId = cardId,
preflightReadFilter = null,
)
) {
is CompletionResult.Success -> {
val key = wallet.publicKey.blockchainKey.toDecompressedPublicKey()
getBnbResultString(
key.toHexString(),
result.data.signature.toHexString(),
)
}
is CompletionResult.Failure -> {
Timber.e(result.error.customMessage)
null
}
}
}
fun prepareDataForPersonalSign(
message: WcSignMessage,
topic: String,
metaName: String,
id: Long,
): WcPersonalSignData {
val messageData = when (message.type) {
WcSignMessage.WCSignType.MESSAGE,
WcSignMessage.WCSignType.PERSONAL_MESSAGE,
-> createMessageData(message)
WcSignMessage.WCSignType.TYPED_MESSAGE -> EthereumUtils.makeTypedDataHash(message.data)
WcSignMessage.WCSignType.SOLANA_MESSAGE -> message.data.decodeBase58()
}
val messageString = message.data.hexToAscii()
?: EthSignHelper.tryToParseEthTypedMessageString(message.data)
?: message.data
val dialogData = PersonalSignDialogData(
dAppName = metaName,
message = messageString,
topic = topic,
id = id,
)
return WcPersonalSignData(
hash = requireNotNull(messageData) { "Message data must not be null" },
topic = topic,
id = id,
dialogData = dialogData,
type = message.type,
)
}
private fun createMessageData(message: WcSignMessage): ByteArray = LegacySdkHelper.createMessageData(message.data)
private fun String.hexToAscii(): String? = LegacySdkHelper.hexToAscii(hex = this)
suspend fun signPersonalMessage(
hashToSign: ByteArray,
networkId: String,
type: WcSignMessage.WCSignType,
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(
runnable = command,
cardId = cardId,
preflightReadFilter = null,
)
) {
is CompletionResult.Success -> {
val signedHash = result.data.signature
return when (type) {
WcSignMessage.WCSignType.SOLANA_MESSAGE -> getSolanaResultString(signedHash)
else -> UnmarshalHelper.unmarshalSignatureExtended(
signature = signedHash,
hash = hashToSign,
publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(),
).asRSVLegacyEVM().toHexString().formatHex()
.lowercase() // use lowercase because some dapps cant handle UPPERCASE
}
}
is CompletionResult.Failure -> {
Timber.e(result.error.customMessage)
null
}
}
}
/**
* Returns result of signing prepared to send in WC request
*/
suspend fun signTransaction(
hashToSign: ByteArray,
networkId: String,
type: TransactionType,
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(
runnable = command,
cardId = cardId,
preflightReadFilter = null,
)
) {
is CompletionResult.Success -> {
val signedHash = result.data.signature
when (type) {
TransactionType.SOLANA_TX -> {
getSolanaResultString(signedHash)
}
}
}
is CompletionResult.Failure -> {
Timber.e(result.error.customMessage)
null
}
}
}
/**
* Returns result of signing prepared to send in WC request
*/
suspend fun signTransactions(
hashesToSign: List<ByteArray>,
networkId: String,
type: TransactionType,
derivationPath: String?,
cardId: String?,
): String? {
val blockchain = Blockchain.fromNetworkId(networkId) ?: return null
val wallet = getWalletManager(blockchain, derivationPath)?.wallet ?: return null
val sdk = store.inject(DaggerGraphState::cardSdkConfigRepository).sdk
val signer = store.inject(DaggerGraphState::transactionSignerFactory).createTransactionSigner(
cardId = cardId,
sdk = sdk,
twinKey = null,
)
return when (val signingResult = signer.sign(hashesToSign, wallet.publicKey)) {
is CompletionResult.Failure -> {
Timber.e(signingResult.error.customMessage)
null
}
is CompletionResult.Success -> {
when (type) {
TransactionType.SOLANA_TX -> {
val result = signingResult.data.mapIndexed { index, bytes ->
byteArrayOf(1) + bytes + hashesToSign[index]
}
getSolanaResultTxHashesString(result)
}
}
}
else -> {
null
}
}
}
private suspend fun getSelectedWallet(): UserWallet? {
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
userWalletsListRepository.selectedUserWalletSync()
} else {
userWalletsListManager.selectedUserWalletSync
}
}
private fun getSolanaResultString(signedHash: ByteArray) = "{ signature: \"${signedHash.encodeBase58()}\" }"
/**
* Build json object
* {
* "transactions": [
* "signed_tx_hash"
* ]
* }
*/
private fun getSolanaResultTxHashesString(signedHashes: List<ByteArray>): String {
val result = JSONObject()
val transactions = JSONArray()
signedHashes.forEach {
transactions.put(it.encodeBase64())
}
result.put("transactions", transactions)
return result.toString()
}
private fun getBnbResultString(publicKey: String, signature: String) =
"{\"signature\":\"$signature\",\"publicKey\":\"$publicKey\"}"
private companion object {
const val HEX_PREFIX = "0x"
const val DEFAULT_MAX_GASLIMIT = 350000
// TODO remove after [REDACTED_JIRA]
private val MANTLE_FEE_ESTIMATE_MULTIPLIER = BigDecimal("1.8")
}
}

View file

@ -1,119 +0,0 @@
package com.tangem.tap.domain.walletconnect2.app
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.tap.domain.walletconnect2.domain.WcBlockchainHelper
import com.tangem.domain.walletconnect.model.legacy.Account
internal class TangemWcBlockchainHelper : WcBlockchainHelper {
private val supportedNonEvmBlockchains = setOf(Blockchain.Solana, Blockchain.SolanaTestnet)
override fun chainIdToNetworkIdOrNull(chainId: String): String? {
val parsedId = chainId.parseId() ?: return null
val blockchain = parsedId.chainIdToBlockchain()
return blockchain?.toNetworkId()
}
override fun chainIdsToBlockchains(chainIds: List<String>): List<Blockchain> {
return chainIds.mapNotNull {
it.parseId()?.chainIdToBlockchain()
}.distinct()
}
override fun chainIdToMissingNetworkNameOrNull(chainId: String): String? {
val parsedId = chainId.parseId() ?: return null
val blockchain = parsedId.chainIdToBlockchain()
return blockchain?.fullName
?: if (parsedId.first == EVM_NAMESPACE) {
chainId
} else {
parsedId.first.replaceFirstChar(Char::titlecase)
}
}
override fun networkIdToChainIdOrNull(networkId: String): List<String> {
val blockchain = Blockchain.fromNetworkId(networkId)
val namespace = blockchain?.getCaip2Namespace() ?: return emptyList()
return blockchain.getCaip2ChainIds().map {
"$namespace$CHAIN_SEPARATOR$it"
}
}
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
}
override fun chainIdsToAccounts(
walletAddress: String,
chainIds: List<String>,
derivationPath: String?,
): List<Account> {
return chainIds.map { chainId ->
Account(chainId, walletAddress, derivationPath)
}
}
private fun Blockchain.getCaip2ChainIds(): List<String> {
if (this.isEvm()) return listOfNotNull(this.getChainId()?.toString())
return when (this) {
/*
* The WC sample application and documentation use a commented out chain ID. However, in real dApps
* uncommented is used.
* Docs: https://docs.walletconnect.com/advanced/multichain/chain-list
*
* */
Blockchain.Solana -> listOf("5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ")
Blockchain.SolanaTestnet -> listOf("z4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z")
Blockchain.Polkadot -> listOf("91b171bb158e2d3848fa23a9f1c25182")
Blockchain.Tron -> listOf("0x2b6653dc")
else -> emptyList()
}
}
private fun Blockchain.getCaip2Namespace(): String? {
return when {
this.isEvm() -> EVM_NAMESPACE
supportedNonEvmBlockchains.contains(this) -> this.toNetworkId().substringBefore(TESTNET_SEPARATOR)
else -> null
}
}
private fun String.parseId(): Pair<String, String>? {
val parsed = this.split(CHAIN_SEPARATOR)
if (parsed.size != 2) return null
return parsed[0] to parsed[1]
}
private fun Pair<String, String>.chainIdToBlockchain(): Blockchain? {
return when (first) {
EVM_NAMESPACE -> {
second.toIntOrNull()
?.let(Blockchain::fromChainId)
}
SOLANA_NAMESPACE -> Blockchain.Solana
else -> {
Blockchain.fromNetworkId(networkId = first)
.takeIf(supportedNonEvmBlockchains::contains)
}
}
}
private companion object {
const val EVM_NAMESPACE = "eip155"
const val SOLANA_NAMESPACE = "solana"
const val CHAIN_SEPARATOR = ":"
const val TESTNET_SEPARATOR = "/"
}
}

View file

@ -1,53 +0,0 @@
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
internal 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(proposal)) },
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)
}
override fun onPairConnectError(error: Throwable) {
store.dispatchOnMain(WalletConnectAction.PairConnectErrorAction(error))
}
}

View file

@ -1,686 +0,0 @@
package com.tangem.tap.domain.walletconnect2.data
import android.app.Application
import arrow.core.flatten
import com.reown.android.Core
import com.reown.android.CoreClient
import com.reown.android.relay.ConnectionType
import com.reown.walletkit.client.Wallet
import com.reown.walletkit.client.WalletKit
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.pair.UnsupportedDApps
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.model.WcPairRequest
import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcMethods
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer
import com.tangem.tap.domain.walletconnect2.domain.WcRequest
import com.tangem.tap.domain.walletconnect2.domain.models.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.emptyFlow
import timber.log.Timber
internal class DefaultLegacyWalletConnectRepositoryFacade constructor(
private val stub: LegacyWalletConnectRepositoryStub,
private val legacy: DefaultLegacyWalletConnectRepository,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
private val wcInitializeUseCase: WcInitializeUseCase,
private val wcPairService: WcPairService,
) : LegacyWalletConnectRepository {
private val isNewWc by lazy {
walletConnectFeatureToggles.isRedesignedWalletConnectEnabled
}
override val events: Flow<WalletConnectEvents> by lazy {
if (isNewWc) stub.events else legacy.events
}
override val activeSessions: Flow<List<WalletConnectSession>> by lazy {
if (isNewWc) stub.activeSessions else legacy.activeSessions
}
override val currentSessions: List<WalletConnectSession> by lazy {
if (isNewWc) stub.currentSessions else legacy.currentSessions
}
override fun init(projectId: String) {
if (isNewWc) {
wcInitializeUseCase.init(projectId)
} else {
legacy.init(projectId)
}
}
override fun setUserNamespaces(userNamespaces: Map<NetworkNamespace, List<Account>>) {
if (isNewWc) stub.setUserNamespaces(userNamespaces) else legacy.setUserNamespaces(userNamespaces)
}
override fun updateSessions() {
if (isNewWc) stub.updateSessions() else legacy.updateSessions()
}
override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) {
val src = when (source) {
SourceType.QR -> WcPairRequest.Source.QR
SourceType.DEEPLINK -> WcPairRequest.Source.DEEPLINK
SourceType.CLIPBOARD -> WcPairRequest.Source.CLIPBOARD
SourceType.ETC -> WcPairRequest.Source.ETC
}
if (isNewWc) {
wcPairService.pair(WcPairRequest(uri = uri, source = src, userWalletId = userWalletId))
} else {
legacy.pair(userWalletId = userWalletId, uri = uri, source = source)
}
}
override fun disconnect(topic: String) {
if (isNewWc) stub.disconnect(topic) else legacy.disconnect(topic)
}
override fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>, blockchainNames: List<String>) {
if (isNewWc) stub.approve(userNamespaces, blockchainNames) else legacy.approve(userNamespaces, blockchainNames)
}
override fun reject() {
if (isNewWc) stub.reject() else legacy.reject()
}
override fun sendRequest(requestData: RequestData, result: String) {
if (isNewWc) stub.sendRequest(requestData, result) else legacy.sendRequest(requestData, result)
}
override fun rejectRequest(requestData: RequestData, error: WalletConnectError) {
if (isNewWc) stub.rejectRequest(requestData, error) else legacy.rejectRequest(requestData, error)
}
override fun cancelRequest(topic: String, id: Long, message: String) {
if (isNewWc) stub.cancelRequest(topic, id, message) else legacy.cancelRequest(topic, id, message)
}
}
internal class LegacyWalletConnectRepositoryStub : LegacyWalletConnectRepository {
override val events: Flow<WalletConnectEvents> = emptyFlow()
override val activeSessions: Flow<List<WalletConnectSession>> = emptyFlow()
override val currentSessions: List<WalletConnectSession> = listOf()
override fun init(projectId: String) = Unit
override fun setUserNamespaces(userNamespaces: Map<NetworkNamespace, List<Account>>) = Unit
override fun updateSessions() = Unit
override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) = Unit
override fun disconnect(topic: String) = Unit
override fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>, blockchainNames: List<String>) = Unit
override fun reject() = Unit
override fun sendRequest(requestData: RequestData, result: String) = Unit
override fun rejectRequest(requestData: RequestData, error: WalletConnectError) = Unit
override fun cancelRequest(topic: String, id: Long, message: String) = Unit
}
@Suppress("LargeClass")
internal class DefaultLegacyWalletConnectRepository(
private val application: Application,
private val wcRequestDeserializer: WcJrpcRequestsDeserializer,
private val analyticsHandler: AnalyticsEventHandler,
) : LegacyWalletConnectRepository {
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
private val blockchainHelper by lazy { TangemWcBlockchainHelper() }
override var currentSessions: List<WalletConnectSession> = emptyList()
private set
/**
* @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.e("Error while initializing client: $error")
scope.launch {
_events.emit(
WalletConnectEvents.SessionApprovalError(
WalletConnectError.ExternalApprovalError(error.throwable.message),
),
)
}
}
WalletKit.initialize(
Wallet.Params.Init(core = CoreClient),
onSuccess = {
val walletDelegate = defineWalletDelegate()
WalletKit.setWalletDelegate(walletDelegate)
},
onError = { error ->
Timber.e("Error while initializing Web3Wallet: $error")
scope.launch {
_events.emit(
WalletConnectEvents.SessionApprovalError(
WalletConnectError.ExternalApprovalError(error.throwable.message),
),
)
}
},
)
}
private fun defineWalletDelegate(): WalletKit.WalletDelegate {
return object : WalletKit.WalletDelegate {
@Suppress("LongMethod")
override fun onSessionProposal(
sessionProposal: Wallet.Model.SessionProposal,
verifyContext: Wallet.Model.VerifyContext,
) {
// Triggered when wallet receives the session proposal sent by a Dapp
Timber.i("sessionProposal: $sessionProposal")
this@DefaultLegacyWalletConnectRepository.sessionProposal = sessionProposal
if (sessionProposal.name in UnsupportedDApps.list) {
Timber.i("Unsupported DApp")
scope.launch {
_events.emit(
WalletConnectEvents.SessionApprovalError(
WalletConnectError.UnsupportedDApp,
),
)
}
return
}
val missingNetworks = findMissingNetworks(
namespaces = sessionProposal.requiredNamespaces,
userNamespaces = this@DefaultLegacyWalletConnectRepository.userNamespaces ?: emptyMap(),
)
if (missingNetworks.isNotEmpty()) {
Timber.i("Not added blockchains: $missingNetworks")
scope.launch {
_events.emit(
WalletConnectEvents.SessionApprovalError(
WalletConnectError.ApprovalErrorMissingNetworks(missingNetworks.toList()),
),
)
}
return
}
val optionalMissingNetwork = findMissingNetworks(
namespaces = sessionProposal.optionalNamespaces,
userNamespaces = this@DefaultLegacyWalletConnectRepository.userNamespaces ?: emptyMap(),
)
val optionalWithoutMissingNetworks = removeMissingNetworks(
namespaces = sessionProposal.optionalNamespaces,
userNamespaces = this@DefaultLegacyWalletConnectRepository.userNamespaces ?: emptyMap(),
)
// for cases when optionalNamespaces is not empty but we doesn't support none of them
if (optionalMissingNetwork.isNotEmpty() &&
optionalWithoutMissingNetworks.isEmpty() &&
sessionProposal.requiredNamespaces.isEmpty() // if requiredNamespaces is not empty we can connect
) {
Timber.i("Not added optional blockchains: $optionalMissingNetwork")
val unsupportedNetworks = sessionProposal.optionalNamespaces.values
.flatMap { it.chains ?: emptyList() }
.filter { blockchainHelper.chainIdToNetworkIdOrNull(it) == null }
scope.launch {
val error = if (unsupportedNetworks.isNotEmpty()) {
WalletConnectEvents.SessionApprovalError(
WalletConnectError.ApprovalErrorUnsupportedNetwork(unsupportedNetworks),
)
} else {
WalletConnectEvents.SessionApprovalError(
WalletConnectError.ApprovalErrorMissingNetworks(optionalMissingNetwork.toList()),
)
}
_events.emit(error)
}
return
}
val requiredChainIds = sessionProposal.requiredNamespaces.values.flatMap { it.chains ?: emptyList() }
val optionalChainIds = optionalWithoutMissingNetworks.toList()
val networks = (requiredChainIds + optionalChainIds)
.mapNotNull { blockchainHelper.chainIdToFullNameOrNull(it) }
.distinct()
analyticsHandler.send(WalletConnect.DAppConnectionRequested(networks))
scope.launch {
_events.emit(
WalletConnectEvents.SessionProposal(
sessionProposal.name,
sessionProposal.description,
sessionProposal.url,
sessionProposal.icons,
requiredChainIds,
optionalChainIds,
),
)
}
}
override fun onSessionRequest(
sessionRequest: Wallet.Model.SessionRequest,
verifyContext: Wallet.Model.VerifyContext,
) {
// Triggered when a Dapp sends SessionRequest to sign a transaction or a message
Timber.i("sessionRequest: $sessionRequest")
val request = wcRequestDeserializer.deserialize(
method = sessionRequest.request.method,
params = sessionRequest.request.params,
)
Timber.i("sessionRequestParsed: $request")
when (request) {
is WcRequest.AddChain -> {
// we can send approval automatically, because in WC 2.0 the list of chains is approved when
// initial connection is established
sendRequest(
RequestData(
topic = sessionRequest.topic,
requestId = sessionRequest.request.id,
blockchain = sessionRequest.chainId.toString(),
method = WcJrpcMethods.WALLET_ADD_ETHEREUM_CHAIN.code,
),
result = "",
)
}
else -> {
val event = WalletConnect.SignatureRequestReceived(
WalletConnect.RequestHandledParams(
dAppName = sessionRequest.peerMetaData?.name ?: "",
dAppUrl = sessionRequest.peerMetaData?.url ?: "",
methodName = sessionRequest.request.method,
blockchain = sessionRequest.chainId
?.let { blockchainHelper.chainIdToNetworkIdOrNull(it) } ?: "",
),
)
analyticsHandler.send(event)
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 ?: "",
method = sessionRequest.request.method,
),
)
}
}
}
}
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))
updateSessionsInternal().join()
}
}
Timber.i("onSessionDelete: $sessionDelete")
}
override fun onSessionExtend(session: Wallet.Model.Session) {
Timber.i("onSessionExtend: $session")
}
override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) {
// Triggered when wallet receives the session settlement response from Dapp
Timber.i("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(),
),
)
updateSessionsInternal().join()
}
}
}
override fun onSessionUpdateResponse(sessionUpdateResponse: Wallet.Model.SessionUpdateResponse) {
// Triggered when wallet receives the session update response from Dapp
Timber.i("onSessionUpdateResponse: $sessionUpdateResponse")
updateSessionsInternal()
}
override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) {
// Triggered whenever the connection state is changed
Timber.i("onConnectionStateChange: $state")
if (state.isAvailable) updateSessionsInternal()
}
override fun onError(error: Wallet.Model.Error) {
// Triggered whenever there is an issue inside the SDK
Timber.i("onError: $error")
}
}
}
override fun setUserNamespaces(userNamespaces: Map<NetworkNamespace, List<Account>>) {
this.userNamespaces = userNamespaces
}
override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) {
analyticsHandler.send(WalletConnect.NewSessionInitiated(source = source))
WalletKit.pair(
params = Wallet.Params.Pair(uri),
onSuccess = {
Timber.i("Paired successfully: $it")
},
onError = {
Timber.e("Error while pairing: $it")
analyticsHandler.send(WalletConnect.SessionFailed)
scope.launch {
_events.emit(
WalletConnectEvents.PairConnectError(it.throwable),
)
}
},
)
}
override fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>, blockchainNames: List<String>) {
val sessionProposal: Wallet.Model.SessionProposal = requireNotNull(this.sessionProposal)
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 preparedRequiredNamespaces = sessionProposal.requiredNamespaces
.map { requiredNamespace ->
val accountsRequired = requiredNamespace.value.chains
?.mapNotNull { chain -> userChains[chain] }
?.flatten() ?: emptyList()
val optionalNamespace = sessionProposal.optionalNamespaces[requiredNamespace.key]
val accountsOptional = optionalNamespace?.chains
?.mapNotNull { chain -> userChains[chain] }
?.flatten() ?: emptyList()
val methods = (requiredNamespace.value.methods + (optionalNamespace?.methods ?: emptyList()))
.distinct()
requiredNamespace.key to Wallet.Model.Namespace.Session(
accounts = (accountsRequired + accountsOptional).distinct(),
methods = methods,
events = requiredNamespace.value.events,
)
}.toMap()
val sessionApproval = Wallet.Params.SessionApprove(
proposerPublicKey = sessionProposal.proposerPublicKey,
namespaces = preparedRequiredNamespaces.ifEmpty {
sessionProposal.createPreparedOptionalNamespaces(userChains)
}.filterValues { it.accounts.isNotEmpty() && it.chains?.isNotEmpty() == true },
)
Timber.i("Session approval is prepared for sending: $sessionApproval")
WalletKit.approveSession(
params = sessionApproval,
onSuccess = {
Timber.i("Approved successfully: $it")
analyticsHandler.send(
WalletConnect.DAppConnected(
dAppName = sessionProposal.name,
dAppUrl = sessionProposal.url,
blockchainNames = blockchainNames,
),
)
},
onError = {
Timber.e("Error while approving: $it")
analyticsHandler.send(
WalletConnect.DAppConnectionFailed(
dAppName = sessionProposal.name,
dAppUrl = sessionProposal.url,
blockchainNames = blockchainNames,
),
)
scope.launch {
_events.emit(
WalletConnectEvents.SessionApprovalError(
WalletConnectError.ExternalApprovalError(it.throwable.message),
),
)
}
},
)
}
private fun Wallet.Model.SessionProposal.createPreparedOptionalNamespaces(
userChains: Map<String, Set<String>>,
): Map<String, Wallet.Model.Namespace.Session> {
return optionalNamespaces
.map { optionalNamespace ->
val accountsOptional = optionalNamespace.value.chains
?.mapNotNull { chain -> userChains[chain] }
?.flatten() ?: emptyList()
val methods = optionalNamespace.value.methods
optionalNamespace.key to Wallet.Model.Namespace.Session(
accounts = accountsOptional.distinct(),
methods = methods,
chains = userChains.keys
.filter { it.startsWith(optionalNamespace.key) }
.toList(),
events = optionalNamespace.value.events,
)
}
.toMap()
}
override fun sendRequest(requestData: RequestData, result: String) {
val session = currentSessions.find { it.topic == requestData.topic }
// Add Ethereum Chain method is processed without user input, skip logging it
if (requestData.method != WcJrpcMethods.WALLET_ADD_ETHEREUM_CHAIN.code) {
analyticsHandler.send(
WalletConnect.SignatureRequestHandled(
WalletConnect.RequestHandledParams(
dAppName = session?.name ?: "",
dAppUrl = session?.url ?: "",
methodName = requestData.method,
blockchain = requestData.blockchain,
),
),
)
}
val params = Wallet.Params.SessionRequestResponse(
sessionTopic = requestData.topic,
jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcResult(
id = requestData.requestId,
result = result,
),
)
Timber.i("Session request response: $params")
WalletKit.respondSessionRequest(
params = params,
onSuccess = { response ->
Timber.i("Session request responded successfully: $response")
},
onError = { error ->
Timber.e(error.throwable, "Error while responging session request")
val handledParams = WalletConnect.RequestHandledParams(
dAppName = session?.name ?: "",
dAppUrl = session?.url ?: "",
methodName = requestData.method,
blockchain = requestData.blockchain,
errorCode = WalletConnectError.ValidationError.error,
errorDescription = error.throwable.message,
)
analyticsHandler.send(WalletConnect.SignatureRequestFailed(handledParams))
},
)
}
override fun rejectRequest(requestData: RequestData, error: WalletConnectError) {
val session = currentSessions.find { it.topic == requestData.topic }
analyticsHandler.send(
WalletConnect.SignatureRequestFailed(
WalletConnect.RequestHandledParams(
dAppName = session?.name ?: "",
dAppUrl = session?.url ?: "",
methodName = requestData.method,
blockchain = requestData.blockchain,
errorCode = error.toString(),
),
),
)
cancelRequest(requestData.topic, requestData.requestId, error.error)
}
override fun cancelRequest(topic: String, id: Long, message: String) {
WalletKit.respondSessionRequest(
params = Wallet.Params.SessionRequestResponse(
sessionTopic = topic,
jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcError(
id = id,
code = 0,
message = message,
),
),
onSuccess = {},
onError = {},
)
}
override fun reject() {
WalletKit.rejectSession(
params = Wallet.Params.SessionReject(
proposerPublicKey = sessionProposal?.proposerPublicKey ?: "",
reason = "",
),
onSuccess = {
Timber.i("Rejected successfully: $it")
},
onError = {
Timber.e("Error while rejecting: $it")
},
)
}
override fun disconnect(topic: String) {
val session = currentSessions.find { it.topic == topic }
WalletKit.disconnectSession(
params = Wallet.Params.SessionDisconnect(topic),
onSuccess = {
analyticsHandler.send(
WalletConnect.SessionDisconnected(
dAppName = session?.name ?: "",
dAppUrl = session?.url ?: "",
),
)
updateSessionsInternal()
Timber.i("Disconnected successfully: $it")
},
onError = {
Timber.e("Error while disconnecting: $it")
},
)
}
fun send(topic: String, id: Long, data: String) {
WalletKit.respondSessionRequest(
params = Wallet.Params.SessionRequestResponse(
sessionTopic = topic,
jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcResult(
id = id,
result = data,
),
),
onError = {},
onSuccess = {},
)
}
override fun updateSessions() {
updateSessionsInternal()
}
private fun updateSessionsInternal(): Job = scope.launch {
val availableSessions = WalletKit.getListOfActiveSessions()
.map {
WalletConnectSession(
topic = it.topic,
icon = it.metaData?.icons?.firstOrNull(),
name = it.metaData?.name,
url = it.metaData?.url,
)
}
Timber.i("Available sessions: $availableSessions")
currentSessions = 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())
}
private fun removeMissingNetworks(
namespaces: Map<String, Wallet.Model.Namespace.Proposal>,
userNamespaces: Map<NetworkNamespace, List<Account>>,
): Collection<String> {
val wcProvidedChains = namespaces.values.flatMap { it.chains ?: emptyList() }
val userChains = userNamespaces.flatMap { it.value.map { account -> account.chainId } }
return wcProvidedChains.intersect(userChains.toSet())
}
}

View file

@ -1,57 +0,0 @@
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.files.FileReader
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletconnect.model.legacy.Session
import timber.log.Timber
internal class DefaultWalletConnectSessionsRepository(
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.d(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

@ -1,110 +0,0 @@
package com.tangem.tap.domain.walletconnect2.di
import android.app.Application
import com.squareup.moshi.Moshi
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.files.FileReader
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
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.DefaultLegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.data.DefaultLegacyWalletConnectRepositoryFacade
import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectSessionsRepository
import com.tangem.tap.domain.walletconnect2.data.LegacyWalletConnectRepositoryStub
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer
import com.tangem.tap.domain.walletconnect2.toggles.DefaultWalletConnectFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object WalletConnectInteractorModule {
@Provides
@Singleton
fun provideWalletConnectInteractor(
wcRepository: LegacyWalletConnectRepository,
wcSessionsRepository: WalletConnectSessionsRepository,
currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade,
walletConnectFeatureToggles: WalletConnectFeatureToggles,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
getSelectedWalletUseCase: GetSelectedWalletUseCase,
): WalletConnectInteractor {
return WalletConnectInteractor(
handler = WalletConnectEventsHandlerImpl(),
walletConnectRepository = wcRepository,
sessionsRepository = wcSessionsRepository,
sdkHelper = WalletConnectSdkHelper(),
blockchainHelper = TangemWcBlockchainHelper(),
currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade,
getSelectedWalletUseCase = getSelectedWalletUseCase,
dispatchers = coroutineDispatcherProvider,
walletConnectFeatureToggles = walletConnectFeatureToggles,
)
}
}
@Module
@InstallIn(SingletonComponent::class)
internal object WalletConnectModule {
@Provides
@Singleton
fun provideWalletConnectFeatureToggles(featureTogglesManager: FeatureTogglesManager): WalletConnectFeatureToggles {
return DefaultWalletConnectFeatureToggles(featureTogglesManager)
}
@Provides
@Singleton
fun provideWalletConnectRepository(
application: Application,
wcRequestDeserializer: WcJrpcRequestsDeserializer,
analyticsHandler: AnalyticsEventHandler,
walletConnectFeatureToggles: WalletConnectFeatureToggles,
wcInitializeUseCase: WcInitializeUseCase,
wcPairService: WcPairService,
): LegacyWalletConnectRepository {
val legacy = DefaultLegacyWalletConnectRepository(
application = application,
wcRequestDeserializer = wcRequestDeserializer,
analyticsHandler = analyticsHandler,
)
val stub = LegacyWalletConnectRepositoryStub()
return DefaultLegacyWalletConnectRepositoryFacade(
stub,
legacy,
walletConnectFeatureToggles,
wcInitializeUseCase,
wcPairService,
)
}
@Provides
@Singleton
fun provideWalletConnectSessionsRepository(
@SdkMoshi moshi: Moshi,
fileReader: FileReader,
): WalletConnectSessionsRepository {
return DefaultWalletConnectSessionsRepository(
moshi = moshi,
fileReader = fileReader,
)
}
}

View file

@ -1,36 +0,0 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.tap.domain.walletconnect2.domain.models.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType
import kotlinx.coroutines.flow.Flow
interface LegacyWalletConnectRepository {
val events: Flow<WalletConnectEvents>
val activeSessions: Flow<List<WalletConnectSession>>
val currentSessions: List<WalletConnectSession>
fun init(projectId: String)
fun setUserNamespaces(userNamespaces: Map<NetworkNamespace, List<Account>>)
fun updateSessions()
fun pair(userWalletId: UserWalletId, uri: String, source: SourceType)
fun disconnect(topic: String)
fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>, blockchainNames: List<String>)
fun reject()
fun sendRequest(requestData: RequestData, result: String)
fun rejectRequest(requestData: RequestData, error: WalletConnectError)
fun cancelRequest(topic: String, id: Long, message: String = "")
}

View file

@ -1,21 +0,0 @@
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()
fun onPairConnectError(error: Throwable)
}

View file

@ -1,457 +0,0 @@
package com.tangem.tap.domain.walletconnect2.domain
import arrow.core.flatten
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.domain.walletconnect.model.legacy.Session
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.filterNotNull
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
import com.tangem.tap.domain.walletconnect2.domain.models.*
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.Timber
import java.util.Stack
@Suppress("LargeClass", "LongParameterList")
class WalletConnectInteractor(
private val handler: WalletConnectEventsHandler,
private val walletConnectRepository: LegacyWalletConnectRepository,
private val sessionsRepository: WalletConnectSessionsRepository,
private val sdkHelper: WalletConnectSdkHelper,
private val dispatchers: CoroutineDispatcherProvider,
private val walletManagersFacade: WalletManagersFacade,
private val currenciesRepository: CurrenciesRepository,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
val blockchainHelper: WcBlockchainHelper,
) {
private val isNewWc by lazy { walletConnectFeatureToggles.isRedesignedWalletConnectEnabled }
private var isWalletConnectReadyForDeepLinks = false
private val wcScope = CoroutineScope(
SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable ->
Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable")
},
)
private val listenerScope = CoroutineScope(
SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable ->
Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable")
},
)
/** Stack of deeplinks to handle if user wallet is not selected or cryptocurrency statuses are not available */
private val deeplinkStack: Stack<String> = Stack()
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,
)
init {
getSelectedWalletUseCase().onRight { userWalletFlow ->
userWalletFlow
.conflate()
.distinctUntilChanged()
.onEach(::initWithWallet)
.flowOn(dispatchers.io)
.launchIn(wcScope)
}
}
private fun initWithWallet(userWallet: UserWallet) {
if (isNewWc) return
if (userWallet.isMultiCurrency) {
Timber.i("WalletConnect: initialize and setup networks for ${userWallet.walletId}")
startListeningWc(userWallet.walletId.stringValue, getCardId(userWallet))
subscribeOnCurrenciesUpdates(userWallet)
}
}
private fun subscribeOnCurrenciesUpdates(userWallet: UserWallet) {
currenciesRepository.getMultiCurrencyWalletCurrenciesUpdates(userWallet.walletId)
.conflate()
.distinctUntilChanged()
.onEach { currencies ->
setupUserChains(userWallet, currencies)
}
.flowOn(dispatchers.io)
.launchIn(wcScope)
}
private suspend fun setupUserChains(userWallet: UserWallet, currencies: List<CryptoCurrency>) {
val accounts = getAccountsForWc(
userWallet = userWallet,
networks = currencies.map { it.network }.distinct(),
)
setUserChains(accounts)
handleDeeplinkStack(accounts)
}
private fun startListeningWc(userWalletId: String, cardId: String?) {
this.userWalletId = userWalletId
this.cardId = cardId
listenerScope.coroutineContext.cancelChildren()
listenerScope.launch {
launch { subscribeToEvents() }
launch { subscribeToSessions() }
walletConnectRepository.updateSessions()
}
}
private fun setUserChains(accounts: List<Account>) {
val userNamespaces: Map<NetworkNamespace, List<Account>> = accounts
.groupBy { account ->
blockchainHelper.getNamespaceFromFullChainIdOrNull(account.chainId)
?.let { NetworkNamespace(it) }
}.filterNotNull()
walletConnectRepository.setUserNamespaces(userNamespaces)
}
private fun handleDeeplinkStack(accounts: List<Account>) {
runCatching {
if (accounts.isEmpty()) return
isWalletConnectReadyForDeepLinks = true
if (deeplinkStack.empty()) return
val lastDeeplink = deeplinkStack.pop()
val action = WalletConnectAction
.OpenSession(
wcUri = lastDeeplink,
source = WalletConnectAction.OpenSession.SourceType.DEEPLINK,
userWalletId = UserWalletId(userWalletId),
)
store.dispatchOnMain(action)
}.onFailure {
Timber.e("WC deeplink handling failed. $it")
}
}
private suspend fun subscribeToEvents() {
events
.onEach { wcEvent ->
Timber.i("WalletConnect: event: $wcEvent")
when (wcEvent) {
is WalletConnectEvents.SessionProposal -> {
val unsupportedNetworks = wcEvent.requiredChainIds
.filter { blockchainHelper.chainIdToNetworkIdOrNull(it) == null }
if (unsupportedNetworks.isNotEmpty()) {
val error = WalletConnectError.ApprovalErrorUnsupportedNetwork(unsupportedNetworks)
handler.onSessionRejected(error)
return@onEach
}
val networksFormatted = (wcEvent.requiredChainIds + wcEvent.optionalChainIds)
.mapNotNull { blockchainHelper.chainIdToFullNameOrNull(it) }
.distinct()
.toString()
handler.onProposalReceived(proposal = wcEvent, networksFormatted = networksFormatted)
}
is WalletConnectEvents.SessionApprovalError -> {
val error = when (wcEvent.error) {
is WalletConnectError.ApprovalErrorMissingNetworks -> {
val missingNetworks = wcEvent.error.missingChains.mapNotNull {
blockchainHelper.chainIdToMissingNetworkNameOrNull(it)
}
WalletConnectError.ApprovalErrorAddNetwork(missingNetworks)
}
else -> wcEvent.error
}
handler.onSessionRejected(error)
}
is WalletConnectEvents.SessionApprovalSuccess -> {
sessionsRepository.saveSession(
userWallet = userWalletId,
session = Session(
accounts = wcEvent.accounts,
topic = wcEvent.topic,
),
)
walletConnectRepository.updateSessions()
handler.onSessionEstablished()
}
is WalletConnectEvents.SessionDeleted -> {
sessionsRepository.removeSession(userWalletId, wcEvent.topic)
}
is WalletConnectEvents.SessionRequest -> {
handleRequest(wcEvent)
}
is WalletConnectEvents.PairConnectError -> {
handler.onPairConnectError(wcEvent.error)
}
}
}
.flowOn(dispatchers.io)
.collect()
}
private suspend fun subscribeToSessions() {
sessions
.onEach { listOfSessions ->
val relevantTopics = getTopicsForUserWallet(userWalletId, sessionsRepository)
val filteredSessions = filterSessionsForUserWallet(listOfSessions, relevantTopics)
handler.onListOfSessionsUpdated(filteredSessions)
}
.flowOn(dispatchers.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>) {
if (isNewWc) return
Timber.i("Approve session proposal: $accounts")
val userNamespaces: Map<NetworkNamespace, List<Account>> = accounts
.groupBy { account ->
blockchainHelper.getNamespaceFromFullChainIdOrNull(account.chainId)
?.let { NetworkNamespace(it) }
}.filterNotNull()
val blockchainNames = blockchainHelper.chainIdsToBlockchains(userNamespaces.values.flatten().map { it.chainId })
.map { it.fullName }
walletConnectRepository.approve(
userNamespaces = userNamespaces,
blockchainNames = blockchainNames,
)
}
fun rejectSessionProposal() {
if (isNewWc) return
Timber.i("Reject session proposal")
walletConnectRepository.reject()
}
fun disconnectSession(topic: String) {
if (isNewWc) return
Timber.i("Disconnect session: $topic")
walletConnectRepository.disconnect(topic)
}
fun cancelRequest(topic: String, id: Long) {
if (isNewWc) return
Timber.i("Cancel request: $topic, $id")
walletConnectRepository.cancelRequest(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
}
}
val networkId = sessionRequest.chainId?.let { blockchainHelper.chainIdToNetworkIdOrNull(it) } ?: ""
val requestData = RequestData(
topic = sessionRequest.topic,
requestId = sessionRequest.id,
blockchain = networkId,
method = sessionRequest.method,
)
if (error != null) {
walletConnectRepository.rejectRequest(requestData, error)
return
}
when (sessionRequest.request) {
is WcRequest.BnbCancel -> Unit
is WcRequest.BnbTxConfirm -> walletConnectRepository.sendRequest(
requestData = requestData,
result = "",
)
else -> {
currentRequest = sessionRequest
val data = prepareRequestData(sessionRequest).getOrElse { e ->
val wrappedError = e as? WalletConnectError ?: WalletConnectError.UnknownError(
message = e.localizedMessage ?: "Unknown error",
)
walletConnectRepository.rejectRequest(requestData, wrappedError)
handler.onSessionRejected(wrappedError)
return
}
handler.onSessionRequest(data)
}
}
}
suspend fun continueWithRequest(request: WcPreparedRequest) {
if (isNewWc) return
val currentRequest = this.currentRequest
if (currentRequest == null || request.topic != currentRequest.topic) return
val networkId = blockchainHelper.chainIdToNetworkIdOrNull(currentRequest.chainId.orEmpty()) ?: return
val signingResultData = 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,
type = request.preparedRequestData.type,
derivationPath = request.derivationPath,
cardId = cardId,
)
is WcPreparedRequest.SolanaSignTransaction -> sdkHelper.signTransaction(
hashToSign = request.preparedRequestData.hashToSign,
networkId = networkId,
type = request.preparedRequestData.type,
derivationPath = request.derivationPath,
cardId = cardId,
)
is WcPreparedRequest.SolanaSignMultipleTransactions -> sdkHelper.signTransactions(
hashesToSign = request.preparedRequestData.hashesToSign,
networkId = networkId,
type = request.preparedRequestData.type,
derivationPath = request.derivationPath,
cardId = cardId,
)
}
val requestData = RequestData(
topic = request.topic,
requestId = request.requestId,
blockchain = networkId,
method = currentRequest.method,
)
if (signingResultData == null) {
walletConnectRepository.rejectRequest(requestData, WalletConnectError.SigningError)
} else {
walletConnectRepository.sendRequest(
requestData = requestData,
result = signingResultData,
)
}
}
/**
* Handles Wallet Connect deep links.
* If wallet connect is able to handle the deeplink, session is started with deeplink.
* Otherwise, deeplink is stored until wallet connect is ready to handle it.
*
* @param deeplink deeplink to handle
*/
fun addDeeplink(deeplink: String) {
if (isNewWc) return
val deeplinkRegex = Regex(WC_PARAM_REGEX)
val matched = deeplinkRegex.findAll(deeplink)
val sessionTopic = matched.firstOrNull { it.value.contains(WC_TOPIC_QUERY_NAME) }?.groupValues?.lastOrNull()
val isAlreadyActiveSessionTopic = walletConnectRepository.currentSessions.any { session ->
session.topic == sessionTopic
}
if (isAlreadyActiveSessionTopic && sessionTopic != null) {
Timber.i("WC already has an active session topic: $deeplink")
return
}
if (isWalletConnectReadyForDeepLinks) {
val action = WalletConnectAction.OpenSession(
wcUri = deeplink,
source = WalletConnectAction.OpenSession.SourceType.DEEPLINK,
userWalletId = UserWalletId(userWalletId),
)
store.dispatchOnMain(action)
} else {
deeplinkStack.push(deeplink)
}
}
private fun getCardId(userWallet: UserWallet): String? {
return if (userWallet is UserWallet.Cold && userWallet.scanResponse.card.backupStatus?.isActive != true) {
userWallet.cardId
} else { // if wallet has backup, any card from wallet can be used to sign
null
}
}
private suspend fun getAccountsForWc(userWallet: UserWallet, networks: List<Network>): List<Account> {
val walletManagers = networks.mapNotNull {
val blockchain = it.toBlockchain()
walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWallet.walletId,
blockchain = blockchain,
derivationPath = it.derivationPath.value,
)
}
return walletManagers.flatMap {
val wallet = it.wallet
val chainIds = blockchainHelper.networkIdToChainIdOrNull(wallet.blockchain.toNetworkId())
blockchainHelper.chainIdsToAccounts(
walletAddress = wallet.address,
chainIds = chainIds,
derivationPath = wallet.publicKey.derivationPath?.rawPath,
)
}
}
private suspend fun prepareRequestData(
sessionRequest: WalletConnectEvents.SessionRequest,
): Result<WcPreparedRequest> {
return sessionRequestConverter.prepareRequest(sessionRequest, userWalletId)
}
private companion object {
const val WC_TOPIC_QUERY_NAME = "sessionTopic"
const val WC_PARAM_REGEX = "([a-zA-Z\\d-]+)=([a-zA-Z\\d]+)"
}
}

View file

@ -1,20 +0,0 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.walletconnect.model.legacy.Account
interface WcBlockchainHelper {
fun chainIdToNetworkIdOrNull(chainId: String): String?
fun chainIdToMissingNetworkNameOrNull(chainId: String): String?
fun networkIdToChainIdOrNull(networkId: String): List<String>
fun getNamespaceFromFullChainIdOrNull(chainId: String): String?
fun chainIdToFullNameOrNull(chainId: String): String?
fun chainIdsToAccounts(walletAddress: String, chainIds: List<String>, derivationPath: String?): List<Account>
fun chainIdsToBlockchains(chainIds: List<String>): List<Blockchain>
}

View file

@ -1,297 +0,0 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.squareup.moshi.*
import com.tangem.datasource.di.SdkMoshi
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 com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTxConfirmParam
import com.tangem.tap.domain.walletconnect2.domain.models.solana.SolanaSignMessage
import com.tangem.tap.domain.walletconnect2.domain.models.solana.SolanaTransactionRequest
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
internal 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"),
WALLET_ADD_ETHEREUM_CHAIN("wallet_addEthereumChain"),
SOLANA_SIGN_TX("solana_signTransaction"),
SOLANA_SIGN_MESSAGE("solana_signMessage"),
SOLANA_SIGN_ALL_TX("solana_signAllTransactions"),
;
companion object {
fun fromCode(code: String): WcJrpcMethods? = values().firstOrNull { it.code == code }
}
}
@JsonClass(generateAdapter = true)
data class WcSignTransaction(
@Json(name = "network")
val network: Int,
@Json(name = "transaction")
val transaction: String,
) : WcRequestData
@JsonClass(generateAdapter = true)
data class WcSignMessage(
@Json(name = "raw")
val raw: List<String>,
@Json(name = "type")
val type: WCSignType,
) : WcRequestData {
@JsonClass(generateAdapter = false)
enum class WCSignType {
MESSAGE, PERSONAL_MESSAGE, TYPED_MESSAGE, SOLANA_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]`
* - SOLANA_MESSAGE: `[publicKey (address), message]`
*
* 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 WcSignMessageData(
@Json(name = "address")
val address: String,
@Json(name = "message")
val message: String,
) : WcRequestData
@JsonClass(generateAdapter = true)
data class WcAddChain(
@Json(name = "chainId")
val chainId: String,
) : WcRequestData
@JsonClass(generateAdapter = true)
data class WcEthereumTransaction(
@Json(name = "from")
val from: String,
@Json(name = "to")
val to: String?,
@Json(name = "nonce")
val nonce: String?,
@Json(name = "gasPrice")
val gasPrice: String?,
@Json(name = "maxFeePerGas")
val maxFeePerGas: String?,
@Json(name = "maxPriorityFeePerGas")
val maxPriorityFeePerGas: String?,
@Json(name = "gas")
val gas: String?,
@Json(name = "gasLimit")
val gasLimit: String?,
@Json(name = "value")
val value: String?,
@Json(name = "data")
val data: String,
) : WcRequestData
@JsonClass(generateAdapter = true)
data class SolanaTransactionsRequest(
@Json(name = "transactions")
val transactions: List<String>,
) : WcRequestData
interface WcRequestData
data class WcCustomRequestData(val data: String) : WcRequestData
sealed class WcRequest(open val data: WcRequestData) {
data class EthSign(override val data: WcSignMessage) : 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 AddChain(override val data: WcAddChain) : WcRequest(data)
data class CustomRequest(override val data: WcCustomRequestData) : WcRequest(data)
data class SolanaSignRequest(override val data: SolanaTransactionRequest) : WcRequest(data)
data class SolanaSignTransactions(override val data: SolanaTransactionsRequest) : WcRequest(data)
}
@Singleton
internal 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 = WcSignMessage(
raw = deserializedParams,
type = WcSignMessage.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 = WcSignMessage(
raw = deserializedParams,
type = WcSignMessage.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 = WcSignMessage(
deserializedParams,
WcSignMessage.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)
}
WcJrpcMethods.WALLET_ADD_ETHEREUM_CHAIN -> {
val deserializedParams: WcAddChain = moshi.adapter<List<WcAddChain>>(
Types.newParameterizedType(List::class.java, WcAddChain::class.java),
).fromJsonFirstOrNull(params) ?: return customRequest
WcRequest.AddChain(data = deserializedParams)
}
WcJrpcMethods.SOLANA_SIGN_TX -> {
val tx = moshi.adapter(SolanaTransactionRequest::class.java)
.fromJsonOrNull(params)
?: return customRequest
WcRequest.SolanaSignRequest(data = tx)
}
WcJrpcMethods.SOLANA_SIGN_MESSAGE -> {
val signMessage = moshi.adapter(SolanaSignMessage::class.java)
.fromJsonOrNull(params)
?: return customRequest
val data = WcSignMessage(
raw = listOf(signMessage.publicKey, signMessage.message),
type = WcSignMessage.WCSignType.SOLANA_MESSAGE,
)
WcRequest.EthSign(data = data)
}
WcJrpcMethods.SOLANA_SIGN_ALL_TX -> {
val transactionsToSign = moshi.adapter(SolanaTransactionsRequest::class.java)
.fromJsonOrNull(params) ?: return customRequest
WcRequest.SolanaSignTransactions(transactionsToSign)
}
}
}
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

@ -1,68 +0,0 @@
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)
class SolanaSignTransaction(
override val preparedRequestData: GenericTransactionData.SingleHash,
topic: String,
requestId: Long,
derivationPath: String?,
) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath)
class SolanaSignMultipleTransactions(
override val preparedRequestData: GenericTransactionData.MultipleHashes,
topic: String,
requestId: Long,
derivationPath: String?,
) : WcPreparedRequest(preparedRequestData, topic, requestId, derivationPath)
}
sealed interface GenericTransactionData {
val dAppName: String
val type: TransactionType
data class SingleHash(
val hashToSign: ByteArray,
override val dAppName: String,
override val type: TransactionType,
) : GenericTransactionData
data class MultipleHashes(
val hashesToSign: List<ByteArray>,
override val dAppName: String,
override val type: TransactionType,
) : GenericTransactionData
}
enum class TransactionType {
SOLANA_TX,
}

View file

@ -1,184 +0,0 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.blockchain.blockchains.solana.SolanaTransactionHelper
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
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.WalletConnectError
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents
import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType
import okio.ByteString.Companion.decodeBase64
internal class WcSessionRequestConverter(
private val blockchainHelper: WcBlockchainHelper,
private val sessionsRepository: WalletConnectSessionsRepository,
private val sdkHelper: WalletConnectSdkHelper,
) {
@Suppress("LongMethod")
suspend fun prepareRequest(
sessionRequest: WalletConnectEvents.SessionRequest,
userWalletId: String,
): Result<WcPreparedRequest> = runCatching {
val networkId = requireNotNull(blockchainHelper.chainIdToNetworkIdOrNull(sessionRequest.chainId.orEmpty())) {
"Failed to get network ID for chain ID: ${sessionRequest.chainId}"
}
val derivationPath = getDerivationPath(
sessionsRepository = sessionsRepository,
sessionRequest = sessionRequest,
userWalletId = userWalletId,
walletAddress = getWalletAddress(sessionRequest.request),
)
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,
),
)
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,
),
)
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,
)
}
is WcRequest.SolanaSignRequest -> {
val transaction = request.data.transaction
WcPreparedRequest.SolanaSignTransaction(
preparedRequestData = GenericTransactionData.SingleHash(
hashToSign = transaction.prepareSolanaTransaction(),
dAppName = sessionRequest.metaName,
type = TransactionType.SOLANA_TX,
),
topic = sessionRequest.topic,
requestId = sessionRequest.id,
derivationPath = derivationPath,
)
}
is WcRequest.SolanaSignTransactions -> {
val transactions = request.data.transactions
WcPreparedRequest.SolanaSignMultipleTransactions(
preparedRequestData = GenericTransactionData.MultipleHashes(
hashesToSign = transactions.map { it.prepareSolanaTransaction() }, //
dAppName = sessionRequest.metaName,
type = TransactionType.SOLANA_TX,
),
topic = sessionRequest.topic,
requestId = sessionRequest.id,
derivationPath = derivationPath,
)
}
else -> throw WalletConnectError.UnsupportedMethod
}
}
/**
* Input transaction in Base64 string
*/
private fun String.prepareSolanaTransaction(): ByteArray {
val transaction = this.decodeBase64()?.toByteArray() ?: ByteArray(0)
return SolanaTransactionHelper.removeSignaturesPlaceholders(transaction)
}
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
is WcRequest.SolanaSignRequest -> request.data.feePayer
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 {
// if walletAddress == null take first account
it.chainId == sessionRequest.chainId &&
(walletAddress == null || it.walletAddress.lowercase() == walletAddress.lowercase())
}?.derivationPath
}
}

View file

@ -1,10 +0,0 @@
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

@ -1,15 +0,0 @@
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

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

View file

@ -1,8 +0,0 @@
package com.tangem.tap.domain.walletconnect2.domain.models
data class RequestData(
val topic: String,
val requestId: Long,
val method: String,
val blockchain: String,
)

View file

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

View file

@ -1,32 +0,0 @@
package com.tangem.tap.domain.walletconnect2.domain.models
import com.tangem.domain.walletconnect.model.legacy.Account
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 requiredChainIds: List<String>,
val optionalChainIds: 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,
val method: String,
) : WalletConnectEvents
data class PairConnectError(val error: Throwable) : WalletConnectEvents
}

View file

@ -1,8 +0,0 @@
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

@ -1,35 +0,0 @@
package com.tangem.tap.domain.walletconnect2.domain.models.binance
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
@Suppress("LongParameterList")
@JsonClass(generateAdapter = true)
data class WcBinanceCancelOrder(
@Json(name = "account_number")
val accountNumber: String,
@Json(name = "chain_id")
val chainId: String,
@Json(name = "data")
val data: String?,
@Json(name = "memo")
val memo: String?,
@Json(name = "sequence")
val sequence: String,
@Json(name = "source")
val source: String,
@Json(name = "msgs")
val msgs: List<Message>,
) : WcRequestData {
@JsonClass(generateAdapter = true)
data class Message(
@Json(name = "refid")
val refid: String,
@Json(name = "sender")
val sender: String,
@Json(name = "symbol")
val symbol: String,
)
}

View file

@ -1,73 +0,0 @@
package com.tangem.tap.domain.walletconnect2.domain.models.binance
import com.github.salomonbrys.kotson.jsonSerializer
import com.google.gson.JsonObject
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
@Suppress("LongParameterList")
@JsonClass(generateAdapter = true)
data class WcBinanceTradeOrder(
@Json(name = "account_number")
val accountNumber: String,
@Json(name = "chain_id")
val chainId: String,
@Json(name = "data")
val data: String?,
@Json(name = "memo")
val memo: String?,
@Json(name = "sequence")
val sequence: String,
@Json(name = "source")
val source: String,
@Json(name = "msgs")
val msgs: List<Message>,
) : WcRequestData {
@JsonClass(generateAdapter = false)
enum class MessageKey(val key: String) {
ID("id"),
ORDER_TYPE("ordertype"),
PRICE("price"),
QUANTITY("quantity"),
SENDER("sender"),
SIDE("side"),
SYMBOL("symbol"),
TIME_INFORCE("timeinforce"),
}
@JsonClass(generateAdapter = true)
data class Message(
@Json(name = "id")
val id: String,
@Json(name = "orderType")
val orderType: Int,
@Json(name = "price")
val price: Long,
@Json(name = "quantity")
val quantity: Long,
@Json(name = "sender")
val sender: String,
@Json(name = "side")
val side: Int,
@Json(name = "symbol")
val symbol: String,
@Json(name = "timeInforce")
val timeInforce: 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

@ -1,51 +0,0 @@
package com.tangem.tap.domain.walletconnect2.domain.models.binance
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
@Suppress("LongParameterList")
@JsonClass(generateAdapter = true)
data class WcBinanceTransferOrder(
@Json(name = "account_number")
val accountNumber: String,
@Json(name = "chain_id")
val chainId: String,
@Json(name = "data")
val data: String?,
@Json(name = "memo")
val memo: String?,
@Json(name = "sequence")
val sequence: String,
@Json(name = "source")
val source: String,
@Json(name = "msgs")
val msgs: List<Message>,
) : WcRequestData {
@JsonClass(generateAdapter = true)
data class Message(
@Json(name = "inputs")
val inputs: List<Item>,
@Json(name = "outputs")
val outputs: List<Item>,
) {
@JsonClass(generateAdapter = true)
data class Item(
@Json(name = "address")
val address: String,
@Json(name = "coins")
val coins: List<Coin>,
) {
@JsonClass(generateAdapter = true)
data class Coin(
@Json(name = "amount")
val amount: Long,
@Json(name = "denom")
val denom: String,
)
}
}
}

View file

@ -1,13 +0,0 @@
package com.tangem.tap.domain.walletconnect2.domain.models.binance
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
@JsonClass(generateAdapter = true)
data class WcBinanceTxConfirmParam(
@Json(name = "ok")
val ok: Boolean,
@Json(name = "errorMsg")
val errorMsg: String?,
) : WcRequestData

View file

@ -1,13 +0,0 @@
package com.tangem.tap.domain.walletconnect2.domain.models.solana
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class SolanaSignMessage(
@Json(name = "pubkey")
val publicKey: String,
@Json(name = "message")
val message: String,
)

View file

@ -1,14 +0,0 @@
package com.tangem.tap.domain.walletconnect2.domain.models.solana
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.tap.domain.walletconnect2.domain.WcRequestData
@JsonClass(generateAdapter = true)
data class SolanaTransactionRequest(
@Json(name = "feePayer")
val feePayer: String?,
@Json(name = "transaction")
val transaction: String,
) : WcRequestData

View file

@ -1,12 +0,0 @@
package com.tangem.tap.domain.walletconnect2.toggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
internal class DefaultWalletConnectFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : WalletConnectFeatureToggles {
override val isRedesignedWalletConnectEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("WALLET_CONNECT_REDESIGN_ENABLED")
}

View file

@ -1,44 +1,22 @@
package com.tangem.tap.features.demo
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.Action
object DemoHelper {
val config = DemoConfig()
private val disabledActionFeatures = listOf(
WalletConnectAction.StartWalletConnect::class.java,
)
fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId)
fun isTestDemoCard(scanResponse: ScanResponse): Boolean = config.isTestDemoCardId(scanResponse.card.cardId)
fun isDemoCardId(cardId: String): Boolean = config.isDemoCardId(cardId)
fun tryHandle(appState: () -> AppState?, action: Action): Boolean {
fun tryHandle(appState: () -> AppState?): Boolean {
val scanResponse = getScanResponse(appState) ?: return false
if (!scanResponse.isDemoCard()) return false
disabledActionFeatures.firstOrNull { it == action::class.java }?.let {
val uiMessageSender = store.inject(DaggerGraphState::uiMessageSender)
uiMessageSender.send(
DialogMessage(
message = resourceReference(R.string.alert_demo_feature_disabled),
),
)
return true
}
return false
}

View file

@ -39,7 +39,7 @@ class DetailsMiddleware {
val detailsMiddleware: Middleware<AppState> = { _, stateProvider ->
{ next ->
{ action ->
if (!DemoHelper.tryHandle(stateProvider, action)) {
if (!DemoHelper.tryHandle(stateProvider)) {
val detailsState = stateProvider()?.detailsState
if (detailsState != null) {
handleAction(detailsState, action)

View file

@ -1,45 +0,0 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.domain.models.wallet.UserWalletId
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.ui.walletconnect.WcSessionForScreen
import org.rekotlin.Action
sealed class WalletConnectAction : Action {
data class HandleDeepLink(val wcUri: String?) : WalletConnectAction()
data object StartWalletConnect : WalletConnectAction()
data class OpenSession(
val wcUri: String,
val source: SourceType,
val userWalletId: UserWalletId,
) : WalletConnectAction() {
enum class SourceType { QR, DEEPLINK, CLIPBOARD, ETC }
}
data class DisconnectSession(val topic: String) : WalletConnectAction()
data class RejectRequest(val topic: String, val id: Long) : WalletConnectAction()
//region WalletConnect 2.0
data class ApproveProposal(val proposal: WalletConnectEvents.SessionProposal) : WalletConnectAction()
data object RejectProposal : WalletConnectAction()
data 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()
data object RejectUnsupportedRequest : WalletConnectAction()
data class PerformRequestedAction(val sessionRequest: WcPreparedRequest) : WalletConnectAction()
data class PairConnectErrorAction(val throwable: Throwable) : WalletConnectAction()
data object UnsupportedDappRequest : WalletConnectAction()
//endregion WalletConnect 2.0
}

View file

@ -1,202 +0,0 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.common.routing.AppRoute
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
@Suppress("LargeClass")
class WalletConnectMiddleware {
private val walletConnectInteractor: WalletConnectInteractor
get() = store.inject(DaggerGraphState::walletConnectInteractor)
private val walletConnectRepository: LegacyWalletConnectRepository
get() = store.inject(DaggerGraphState::walletConnectRepository)
val walletConnectMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handle(state, action)
next(action)
}
}
}
@Suppress("ComplexMethod", "LongMethod")
private fun handle(state: () -> AppState?, action: Action) {
if (DemoHelper.tryHandle(state, action)) return
when (action) {
is WalletConnectAction.HandleDeepLink -> {
val wsUrl = action.wcUri
Timber.i("WC deeplink: $wsUrl")
if (!wsUrl.isNullOrBlank()) {
Timber.i("WC deeplink added to stack: $wsUrl")
walletConnectInteractor.addDeeplink(wsUrl)
}
}
is WalletConnectAction.DisconnectSession -> {
walletConnectInteractor.disconnectSession(action.topic)
}
is WalletConnectAction.StartWalletConnect -> {
store.dispatchNavigationAction {
push(AppRoute.QrScanning(AppRoute.QrScanning.Source.WalletConnect))
}
}
is WalletConnectAction.OpenSession -> {
val index = action.wcUri.indexOf("@")
when (action.wcUri[index + 1]) {
'2' -> walletConnectRepository.pair(
uri = action.wcUri,
source = action.source,
userWalletId = action.userWalletId,
)
'1' -> {
store.dispatchOnMain(WalletConnectAction.UnsupportedDappRequest)
store.dispatchOnMain(
GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedWcVersion),
)
}
}
}
is WalletConnectAction.RejectRequest -> {
walletConnectInteractor.cancelRequest(action.topic, action.id)
}
is WalletConnectAction.ApproveProposal -> {
scope.launch {
val proposalChainIds = action.proposal.requiredChainIds + action.proposal.optionalChainIds
val proposalBlockchains =
walletConnectInteractor.blockchainHelper.chainIdsToBlockchains(proposalChainIds)
val accounts = getWalletManagers()
.filter { walletManager -> proposalBlockchains.contains(walletManager.wallet.blockchain) }
.flatMap {
val wallet = it.wallet
val chainIds = walletConnectInteractor.blockchainHelper.networkIdToChainIdOrNull(
wallet.blockchain.toNetworkId(),
)
walletConnectInteractor.blockchainHelper.chainIdsToAccounts(
walletAddress = wallet.address,
chainIds = chainIds,
derivationPath = wallet.publicKey.derivationPath?.rawPath,
)
}
walletConnectInteractor.approveSessionProposal(accounts)
}
}
is WalletConnectAction.RejectProposal -> {
walletConnectInteractor.rejectSessionProposal()
}
is WalletConnectAction.SessionEstablished -> {
}
is WalletConnectAction.SessionRejected -> {
when (action.error) {
is WalletConnectError.ApprovalErrorAddNetwork -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.AddNetwork(action.error.networks),
),
)
}
is WalletConnectError.ApprovalErrorUnsupportedNetwork -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.UnsupportedNetwork(action.error.unsupportedNetworks),
),
)
}
is WalletConnectError.UnsupportedDApp -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.UnsupportedDapp,
),
)
}
is WalletConnectError.UnknownError -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
AppDialog.SimpleOkDialogRes(
headerId = R.string.wallet_connect_title,
messageId = R.string.wallet_connect_error_with_framework_message,
args = listOf(action.error.message),
),
),
)
}
is WalletConnectError.ExternalApprovalError -> {
Timber.e(action.error, "ExternalApprovalError ${action.error.message}")
// do not show dialog on this event
// val message = action.error.message
// if (!message.isNullOrEmpty()) {
// store.dispatchOnMain(
// GlobalAction.ShowDialog(
// AppDialog.SimpleOkWarningDialog(
// message = 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)
is WcPreparedRequest.SolanaSignTransaction ->
WalletConnectDialog.SignTransactionDialog(request)
is WcPreparedRequest.SolanaSignMultipleTransactions ->
WalletConnectDialog.SignTransactionsDialog(request)
}
store.dispatch(GlobalAction.ShowDialog(dialog))
}
is WalletConnectAction.PerformRequestedAction -> {
scope.launch { walletConnectInteractor.continueWithRequest(action.sessionRequest) }
}
is WalletConnectAction.RejectUnsupportedRequest -> {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork()))
}
is WalletConnectAction.PairConnectErrorAction -> {
store.dispatch(GlobalAction.ShowDialog(WalletConnectDialog.PairConnectErrorDialog(action.throwable)))
}
}
}
private suspend fun getWalletManagers(): List<WalletManager> {
val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade)
val userWallet = getSelectedWallet() ?: return emptyList()
return walletManagerFacade.getStoredWalletManagers(userWallet.walletId)
}
private suspend fun getSelectedWallet(): UserWallet? {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val hotWalletFeatureToggles = store.inject(DaggerGraphState::hotWalletFeatureToggles)
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
userWalletsListRepository.selectedUserWalletSync()
} else {
userWalletsListManager.selectedUserWalletSync
}
}
}

View file

@ -1,26 +0,0 @@
package com.tangem.tap.features.details.redux.walletconnect
import org.rekotlin.Action
object WalletConnectReducer {
fun reduce(action: Action, state: WalletConnectState): WalletConnectState {
if (action !is WalletConnectAction) return state
return when (action) {
is WalletConnectAction.OpenSession -> {
state.copy(loading = true)
}
is WalletConnectAction.ApproveProposal -> state.copy(loading = true)
is WalletConnectAction.RejectProposal,
is WalletConnectAction.SessionEstablished,
is WalletConnectAction.SessionRejected,
is WalletConnectAction.PairConnectErrorAction,
is WalletConnectAction.UnsupportedDappRequest,
-> state.copy(loading = false)
is WalletConnectAction.SessionListUpdated -> state.copy(
wc2Sessions = action.sessions,
)
else -> state
}
}
}

View file

@ -1,173 +0,0 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage
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
data class WalletConnectState(
val loading: Boolean = false,
val wc2Sessions: List<WcSessionForScreen> = listOf(),
val newSessionData: NewWcSessionData? = null,
)
data class NewWcSessionData(
val session: WalletConnectSession,
val scanResponse: ScanResponse,
val blockchain: Blockchain?,
)
data class WalletConnectSession(
val peerId: String,
val remotePeerId: String?,
val wallet: WalletForSession,
) {
fun getAddress(): String? {
val key = wallet.derivedPublicKey ?: wallet.walletPublicKey ?: return null
return wallet.blockchain?.makeAddresses(key)?.first()?.value
}
}
@JsonClass(generateAdapter = true)
data class WalletForSession(
val walletPublicKey: ByteArray? = null,
val derivedPublicKey: ByteArray? = null,
val derivationPath: DerivationPath? = null,
val derivationStyle: DerivationStyle? = null,
val isTestNet: Boolean = false,
val blockchain: Blockchain? = if (isTestNet) Blockchain.EthereumTestnet else Blockchain.Ethereum,
) {
fun getBlockchainForSession(): Blockchain {
return blockchain ?: if (isTestNet) Blockchain.EthereumTestnet else Blockchain.Ethereum
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as WalletForSession
if (walletPublicKey != null) {
if (other.walletPublicKey == null) return false
if (!walletPublicKey.contentEquals(other.walletPublicKey)) return false
} else if (other.walletPublicKey != null) return false
if (derivedPublicKey != null) {
if (other.derivedPublicKey == null) return false
if (!derivedPublicKey.contentEquals(other.derivedPublicKey)) return false
} else if (other.derivedPublicKey != null) return false
if (derivationPath != other.derivationPath) return false
if (isTestNet != other.isTestNet) return false
return blockchain == other.blockchain
}
override fun hashCode(): Int {
var result = walletPublicKey?.contentHashCode() ?: 0
result = 31 * result + (derivedPublicKey?.contentHashCode() ?: 0)
result = 31 * result + (derivationPath?.hashCode() ?: 0)
result = 31 * result + isTestNet.hashCode()
result = 31 * result + (blockchain?.hashCode() ?: 0)
return result
}
}
sealed class WalletConnectDialog : StateDialog {
data object UnsupportedWcVersion : WalletConnectDialog()
data object UnsupportedCard : WalletConnectDialog()
data class UnsupportedNetwork(val networks: List<String>? = null) : WalletConnectDialog()
data object UnsupportedDapp : WalletConnectDialog()
data class AddNetwork(val networks: List<String>) : WalletConnectDialog()
data object OpeningSessionRejected : WalletConnectDialog()
data object SessionTimeout : WalletConnectDialog()
data class ApproveWcSession(
val session: WalletConnectSession,
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 data: WcPreparedRequest.EthTransaction) :
WalletConnectDialog()
data class PersonalSign(val data: WcPreparedRequest.EthSign) : WalletConnectDialog()
data class BnbTransactionDialog(
val data: WcPreparedRequest.BnbTransaction,
) : WalletConnectDialog()
data class SignTransactionDialog(
val data: WcPreparedRequest.SolanaSignTransaction,
) : WalletConnectDialog()
data class SignTransactionsDialog(
val data: WcPreparedRequest.SolanaSignMultipleTransactions,
) : WalletConnectDialog()
data class PairConnectErrorDialog(val error: Throwable) : WalletConnectDialog()
}
data class WcTransactionData(
val type: WcEthTransactionType,
val transaction: TransactionData,
val topic: String,
val id: Long,
val walletManager: WalletManager,
val dialogData: TransactionRequestDialogData,
)
enum class WcEthTransactionType {
EthSignTransaction,
EthSendTransaction,
}
data class WcPersonalSignData(
val hash: ByteArray,
val topic: String,
val id: Long,
val dialogData: PersonalSignDialogData,
val type: WcSignMessage.WCSignType,
)
sealed class BinanceMessageData(
val address: String,
val data: ByteArray,
) {
class Trade(
val tradeData: List<TradeData>,
address: String,
data: ByteArray,
) : BinanceMessageData(address, data)
class Transfer(
val outputAddress: String,
val amount: String,
address: String,
data: ByteArray,
) : BinanceMessageData(address, data)
}
data class TradeData(
val price: String,
val quantity: String,
val amount: String,
val symbol: String,
)

View file

@ -1,73 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import com.arkivanov.essenty.lifecycle.subscribe
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent
import com.tangem.tap.store
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import org.rekotlin.StoreSubscriber
@Suppress("UnusedPrivateMember")
internal class DefaultWalletConnectComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: WalletConnectComponent.Params,
) : WalletConnectComponent, AppComponentContext by appComponentContext, StoreSubscriber<WalletConnectState> {
private val model: WalletConnectModel = getOrCreateModel(params)
private var screenState: MutableState<WalletConnectScreenState> =
mutableStateOf(model.updateState(store.state.walletConnectState))
init {
lifecycle.subscribe(
onCreate = {
Analytics.send(WalletConnect.ScreenOpened())
},
onStart = {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.walletConnectState == newState.walletConnectState
}.select { it.walletConnectState }
}
},
onStop = {
store.unsubscribe(this)
},
)
}
override fun newState(state: WalletConnectState) {
screenState.value = model.updateState(state)
}
@Composable
override fun Content(modifier: Modifier) {
WalletConnectScreen(
modifier = modifier,
state = screenState.value,
onBackClick = {
store.dispatchNavigationAction(AppRouter::pop)
},
)
}
@AssistedFactory
interface Factory : WalletConnectComponent.Factory {
override fun create(
context: AppComponentContext,
params: WalletConnectComponent.Params,
): DefaultWalletConnectComponent
}
}

View file

@ -1,72 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.qrscanning.models.QrResultSource
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Stable
@ModelScoped
internal class WalletConnectModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
paramsContainer: ParamsContainer,
) : Model() {
private val params = paramsContainer.require<WalletConnectComponent.Params>()
init {
modelScope.launch {
listenToQrScanningUseCase.listen(SourceType.WALLET_CONNECT)
.getOrElse { emptyFlow() }
.map { result ->
val source = when (result.resultSource) {
QrResultSource.CLIPBOARD -> WalletConnectAction.OpenSession.SourceType.CLIPBOARD
QrResultSource.CAMERA,
QrResultSource.GALLERY,
-> WalletConnectAction.OpenSession.SourceType.QR
}
WalletConnectAction.OpenSession(
wcUri = result.qrCode,
source = source,
userWalletId = params.userWalletId,
)
}
.collect { store.dispatch(it) }
}
}
fun updateState(state: WalletConnectState): WalletConnectScreenState {
Timber.d("WC2 Sessions: ${state.wc2Sessions}")
val sessions = state.wc2Sessions
return WalletConnectScreenState(
sessions.toImmutableList(),
isLoading = state.loading,
onRemoveSession = { sessionUri -> onRemoveSession(sessionUri, sessions) },
onAddSession = {
store.dispatch(WalletConnectAction.StartWalletConnect)
},
)
}
private fun onRemoveSession(sessionUri: String, wc2sessions: List<WcSessionForScreen>) {
wc2sessions.firstOrNull { it.sessionId == sessionUri }?.let {
store.dispatch(WalletConnectAction.DisconnectSession(sessionUri))
}
}
}

View file

@ -1,169 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect
import androidx.compose.foundation.Image
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.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun WalletConnectScreen(
state: WalletConnectScreenState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
SettingsScreensScaffold(
modifier = modifier,
content = {
if (state.sessions.isEmpty()) {
EmptyScreen(state)
} else {
WalletConnectSessions(state)
}
},
fab = {
if (!state.isLoading) {
AddSessionFab(
onAddSession = {
Analytics.send(Settings.ButtonStartWalletConnectSession())
state.onAddSession()
},
)
}
},
titleRes = R.string.wallet_connect_title,
onBackClick = onBackClick,
)
}
@Composable
private fun AddSessionFab(onAddSession: () -> Unit, modifier: Modifier = Modifier) {
FloatingActionButton(
onClick = onAddSession,
containerColor = TangemTheme.colors.button.primary,
contentColor = TangemTheme.colors.icon.primary2,
shape = RoundedCornerShape(16.dp),
modifier = modifier,
) {
Icon(
painter = painterResource(id = R.drawable.ic_plus_24),
contentDescription = "",
)
}
}
@Composable
private fun EmptyScreen(state: WalletConnectScreenState) {
if (state.isLoading) {
TangemLinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
color = TangemTheme.colors.icon.accent,
)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(bottom = 64.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(id = R.drawable.ic_wallet_connect_24),
contentDescription = "",
colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive),
contentScale = ContentScale.FillWidth,
modifier = Modifier.width(width = 100.dp),
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResourceSafe(id = R.string.wallet_connect_subtitle),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
}
@Composable
private fun WalletConnectSessions(state: WalletConnectScreenState) {
if (state.isLoading) {
TangemLinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.height(2.dp),
color = TangemTheme.colors.icon.accent,
)
} else {
Spacer(modifier = Modifier.height(2.dp))
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
items(state.sessions) { session ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = session.description,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.weight(1f),
)
IconButton(
onClick = {
Analytics.send(Settings.ButtonStopWalletConnectSession())
state.onRemoveSession(session.sessionId)
},
) {
Icon(
painter = painterResource(id = R.drawable.ic_cross_rounded_24),
contentDescription = "",
tint = TangemTheme.colors.icon.warning,
)
}
}
}
}
}
@Composable
@Preview
private fun WalletConnectScreenPreview() {
WalletConnectScreen(
state = WalletConnectScreenState(
sessions = persistentListOf(
WcSessionForScreen(
description = "session from some dApp",
sessionId = "",
),
),
isLoading = true,
),
{},
)
}

View file

@ -1,15 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect
import kotlinx.collections.immutable.ImmutableList
internal data class WalletConnectScreenState(
val sessions: ImmutableList<WcSessionForScreen>,
val isLoading: Boolean = false,
val onRemoveSession: (String) -> Unit = {},
val onAddSession: () -> Unit = {},
)
data class WcSessionForScreen(
val description: String,
val sessionId: String,
)

View file

@ -1,10 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.wallet.UserWalletId
interface WalletConnectComponent : ComposableContentComponent {
data class Params(val userWalletId: UserWalletId)
interface Factory : ComponentFactory<Params, WalletConnectComponent>
}

View file

@ -1,25 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect.di
import com.tangem.core.decompose.model.Model
import com.tangem.tap.features.details.ui.walletconnect.DefaultWalletConnectComponent
import com.tangem.tap.features.details.ui.walletconnect.WalletConnectModel
import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(SingletonComponent::class)
internal interface WalletConnectFeatureModule {
@Binds
fun bindFactory(impl: DefaultWalletConnectComponent.Factory): WalletConnectComponent.Factory
@Binds
@IntoMap
@ClassKey(WalletConnectModel::class)
fun bindModel(model: WalletConnectModel): Model
}

View file

@ -1,55 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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
object BnbTransactionDialog {
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(
R.string.wallet_connect_bnb_trade_order_message,
it.symbol,
it.price,
it.quantity,
it.amount,
)
}.joinToString(separator = "\n\n")
is BinanceMessageData.Transfer -> context.getString(
R.string.wallet_connect_bnb_transaction_message,
data.address,
data.outputAddress,
data.amount,
)
}
val fullMessage = context.getString(
R.string.wallet_connect_bnb_sign_message,
preparedData.preparedRequestData.dAppName,
message,
)
val positiveButtonTitle = context.getText(R.string.common_sign)
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(context.getString(R.string.wallet_connect_title))
setMessage(fullMessage)
setPositiveButton(positiveButtonTitle) { _, _ ->
store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData))
}
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
store.dispatch(WalletConnectAction.RejectRequest(preparedData.topic, preparedData.requestId))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -1,37 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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
object PersonalSignDialog {
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 MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(context.getString(R.string.wallet_connect_title))
setMessage(message)
setPositiveButton(context.getText(R.string.common_sign)) { _, _ ->
store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData))
}
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
store.dispatch(WalletConnectAction.RejectRequest(data.topic, data.id))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}
data class PersonalSignDialogData(
val dAppName: String,
val message: String,
val topic: String,
val id: Long,
)

View file

@ -1,42 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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 MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).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

@ -1,30 +0,0 @@
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
internal object SignTransactionDialog {
fun create(preparedData: WcPreparedRequest.SolanaSignTransaction, context: Context): AlertDialog {
val signMessage = context.getString(R.string.wallet_connect_alert_sign_message, "")
val message = "${preparedData.preparedRequestData.dAppName}\n$signMessage"
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.PerformRequestedAction(preparedData))
}
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
store.dispatch(WalletConnectAction.RejectRequest(preparedData.topic, preparedData.requestId))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -1,30 +0,0 @@
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
internal object SignTransactionsDialog {
fun create(preparedData: WcPreparedRequest.SolanaSignMultipleTransactions, context: Context): AlertDialog {
val signMessage = context.getString(R.string.wallet_connect_alert_sign_message, "")
val message = "${preparedData.preparedRequestData.dAppName}\n$signMessage"
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.PerformRequestedAction(preparedData))
}
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
store.dispatch(WalletConnectAction.RejectRequest(preparedData.topic, preparedData.requestId))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -1,61 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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.WcEthTransactionType
import com.tangem.tap.store
import com.tangem.wallet.R
object TransactionDialog {
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,
data.dAppUrl,
data.amount,
data.feeAmount,
data.totalAmount,
data.balance,
)
val positiveButtonTitle = when (data.type) {
WcEthTransactionType.EthSignTransaction -> context.getText(R.string.common_sign)
WcEthTransactionType.EthSendTransaction -> context.getText(R.string.common_sign_and_send)
}
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(context.getString(R.string.wallet_connect_title))
setMessage(message)
setPositiveButton(positiveButtonTitle) { _, _ ->
if (data.isEnoughFundsToSend) {
store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData))
} else {
store.dispatch(WalletConnectAction.RejectRequest(data.topic, data.id))
}
}
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
store.dispatch(WalletConnectAction.RejectRequest(data.topic, data.id))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}
data class TransactionRequestDialogData(
val dAppName: String,
val dAppUrl: String,
val amount: String,
val feeAmount: String,
val totalAmount: String,
val balance: String,
val isEnoughFundsToSend: Boolean,
val topic: String,
val id: Long,
val type: WcEthTransactionType,
)

View file

@ -1,48 +0,0 @@
package com.tangem.tap.features.intentHandler.handlers
import android.content.Intent
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.removePrefixOrNull
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.intentHandler.AffectsNavigation
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.store
import timber.log.Timber
import java.net.URLDecoder
/**
[REDACTED_AUTHOR]
*/
class WalletConnectLinkIntentHandler : IntentHandler, AffectsNavigation {
override fun handleIntent(intent: Intent?, isFromForeground: Boolean): Boolean {
val intentData = intent?.data ?: return false
val scheme = intent.scheme ?: return false
val wcUri = when (scheme) {
WC_SCHEME -> intentData.toString()
TANGEM_SCHEME -> intentData.toString().removePrefixOrNull(TANGEM_WC_PREFIX)
else -> null
}
return if (wcUri.isNullOrBlank()) {
false
} else {
val decodedWcUri = try {
URLDecoder.decode(wcUri, DEFAULT_CHARSET_NAME)
} catch (e: Exception) {
Timber.e(e)
return false
}
store.dispatchOnMain(WalletConnectAction.HandleDeepLink(decodedWcUri))
true
}
}
private companion object {
const val TANGEM_SCHEME = "tangem"
const val TANGEM_WC_PREFIX = "tangem://wc?uri="
const val DEFAULT_CHARSET_NAME = "UTF-8"
const val WC_SCHEME = "wc"
}
}

View file

@ -2,12 +2,15 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.Onboarding.*
import com.tangem.tap.common.extensions.*
import com.tangem.tap.backupService
import com.tangem.tap.common.analytics.events.Onboarding.Finished
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
@ -24,7 +27,7 @@ class BackupMiddleware {
@Suppress("LongMethod", "ComplexMethod", "MagicNumber")
private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) {
if (DemoHelper.tryHandle(appState, action)) return
if (DemoHelper.tryHandle(appState)) return
when (action) {
is BackupAction.DiscardBackup -> {

View file

@ -30,7 +30,7 @@ object TradeCryptoMiddleware {
}
private fun handle(state: () -> AppState?, action: TradeCryptoAction) {
if (DemoHelper.tryHandle(state, action)) return
if (DemoHelper.tryHandle(state)) return
when (action) {
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)

View file

@ -20,7 +20,6 @@ import com.tangem.domain.wallets.legacy.unlockIfLockable
import com.tangem.tap.*
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.proxy.redux.DaggerGraphState
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
@ -32,21 +31,19 @@ internal class WelcomeMiddleware {
{ action ->
val appState = appStateProvider()
if (action is WelcomeAction && appState != null) {
handleAction(action, appState.welcomeState)
handleAction(action)
}
next(action)
}
}
}
private fun handleAction(action: WelcomeAction, state: WelcomeState) {
private fun handleAction(action: WelcomeAction) {
mainScope.launch {
when (action) {
is WelcomeAction.ProceedWithIntent -> proceedWithIntent(action.intent)
is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics(
afterUnlockIntent = action.afterUnlockIntent ?: state.intent,
)
is WelcomeAction.ProceedWithCard -> proceedWithCard(afterScanIntent = state.intent)
is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics()
is WelcomeAction.ProceedWithCard -> proceedWithCard()
is WelcomeAction.ClearUserWallets -> disableUserWalletsSaving()
else -> Unit
}
@ -70,13 +67,8 @@ internal class WelcomeMiddleware {
}
}
private suspend fun proceedWithBiometrics(afterUnlockIntent: Intent?) {
Timber.d(
"""
Proceeding with biometry
|- Intent: $afterUnlockIntent
""".trimIndent(),
)
private suspend fun proceedWithBiometrics() {
Timber.d("Proceeding with biometry")
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
userWalletsListManager.unlockIfLockable(type = UnlockType.ANY)
@ -93,20 +85,11 @@ internal class WelcomeMiddleware {
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Success)
store.onUserWalletSelected(userWallet = selectedUserWallet)
afterUnlockIntent?.let {
WalletConnectLinkIntentHandler().handleIntent(it, false)
}
}
}
private suspend fun proceedWithCard(afterScanIntent: Intent?) {
Timber.d(
"""
Proceeding with card
|- Intent: $afterScanIntent
""".trimIndent(),
)
private suspend fun proceedWithCard() {
Timber.d("Proceeding with card")
scanCardInternal { scanResponse ->
val userWalletBuilder = store.inject(DaggerGraphState::coldUserWalletBuilderFactory).create(scanResponse)
@ -125,10 +108,6 @@ internal class WelcomeMiddleware {
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
store.dispatchWithMain(WelcomeAction.ProceedWithCard.Success)
store.onUserWalletSelected(userWallet = userWallet)
afterScanIntent?.let {
WalletConnectLinkIntentHandler().handleIntent(it, false)
}
}
}
}

View file

@ -2,14 +2,12 @@ package com.tangem.tap.proxy.redux
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import org.rekotlin.Action
sealed interface DaggerGraphAction : Action {
data class SetActivityDependencies(
val scanCardUseCase: ScanCardUseCase,
val walletConnectInteractor: WalletConnectInteractor,
val cardSdkConfigRepository: CardSdkConfigRepository,
) : DaggerGraphAction
}

View file

@ -14,7 +14,6 @@ object DaggerGraphReducer {
return when (action) {
is DaggerGraphAction.SetActivityDependencies -> state.daggerGraphState.copy(
scanCardUseCase = action.scanCardUseCase,
walletConnectInteractor = action.walletConnectInteractor,
cardSdkConfigRepository = action.cardSdkConfigRepository,
)
}

View file

@ -37,8 +37,6 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.proxy.AppStateHolder
import org.rekotlin.StateType
@ -46,8 +44,6 @@ data class DaggerGraphState(
val networkConnectionManager: NetworkConnectionManager? = null,
val cardScanningFeatureToggles: CardScanningFeatureToggles? = null,
val scanCardUseCase: ScanCardUseCase? = null,
val walletConnectRepository: LegacyWalletConnectRepository? = null,
val walletConnectInteractor: WalletConnectInteractor? = null,
val scanCardProcessor: ScanCardProcessor? = null,
val cardSdkConfigRepository: CardSdkConfigRepository? = null,
val appCurrencyRepository: AppCurrencyRepository? = null,

View file

@ -8,22 +8,14 @@ import com.tangem.feature.referral.api.ReferralComponent
import com.tangem.feature.stories.api.StoriesComponent
import com.tangem.feature.usedesk.api.UsedeskComponent
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
import com.tangem.features.account.ArchivedAccountListComponent
import com.tangem.features.account.AccountCreateEditComponent
import com.tangem.features.account.AccountDetailsComponent
import com.tangem.features.account.ArchivedAccountListComponent
import com.tangem.features.createwalletselection.CreateWalletSelectionComponent
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
import com.tangem.features.home.api.HomeComponent
import com.tangem.features.hotwallet.AddExistingWalletComponent
import com.tangem.features.hotwallet.CreateMobileWalletComponent
import com.tangem.features.hotwallet.UpgradeWalletComponent
import com.tangem.features.hotwallet.WalletActivationComponent
import com.tangem.features.hotwallet.CreateWalletBackupComponent
import com.tangem.features.hotwallet.UpdateAccessCodeComponent
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.hotwallet.WalletBackupComponent
import com.tangem.features.hotwallet.ViewPhraseComponent
import com.tangem.features.hotwallet.*
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensSource
@ -45,14 +37,12 @@ import com.tangem.features.tangempay.components.TangemPayDetailsComponent
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.wallet.WalletEntryComponent
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.tap.features.details.ui.appcurrency.api.AppCurrencySelectorComponent
import com.tangem.tap.features.details.ui.appsettings.api.AppSettingsComponent
import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.api.AccessCodeRecoveryComponent
import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent
import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent
import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent
import com.tangem.tap.features.welcome.component.WelcomeComponent
import com.tangem.tap.routing.component.RoutingComponent.Child
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
@ -84,7 +74,6 @@ internal class ChildFactory @Inject constructor(
private val swapComponentFactory: SwapComponent.Factory,
private val homeComponentFactory: HomeComponent.Factory,
private val tokenDetailsComponentFactory: TokenDetailsComponent.Factory,
private val walletConnectComponentFactory: WalletConnectComponent.Factory,
private val qrScanningComponentFactory: QrScanningComponent.Factory,
private val accessCodeRecoveryComponentFactory: AccessCodeRecoveryComponent.Factory,
private val cardSettingsComponentFactory: CardSettingsComponent.Factory,
@ -116,7 +105,6 @@ internal class ChildFactory @Inject constructor(
private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory,
private val tangemPayDetailsComponentFactory: TangemPayDetailsComponent.Factory,
private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) {
@ -329,19 +317,11 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.WalletConnectSessions -> {
if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
createComponentChild(
context = context,
params = RedesignedWalletConnectComponent.Params(route.userWalletId),
componentFactory = redesignedWalletConnectComponentFactory,
)
} else {
createComponentChild(
context = context,
params = WalletConnectComponent.Params(route.userWalletId),
componentFactory = walletConnectComponentFactory,
)
}
createComponentChild(
context = context,
params = RedesignedWalletConnectComponent.Params(route.userWalletId),
componentFactory = redesignedWalletConnectComponentFactory,
)
}
is AppRoute.QrScanning -> {
val source = when (route.source) {