Updated on 2026-08-14
This commit is contained in:
parent
fd7bc522f2
commit
194e750c93
74 changed files with 52 additions and 4627 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = "/"
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = "")
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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]+)"
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
package com.tangem.tap.domain.walletconnect2.domain.models
|
||||
|
||||
@JvmInline
|
||||
value class NetworkNamespace(val value: String)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -14,7 +14,6 @@ object DaggerGraphReducer {
|
|||
return when (action) {
|
||||
is DaggerGraphAction.SetActivityDependencies -> state.daggerGraphState.copy(
|
||||
scanCardUseCase = action.scanCardUseCase,
|
||||
walletConnectInteractor = action.walletConnectInteractor,
|
||||
cardSdkConfigRepository = action.cardSdkConfigRepository,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue