Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-22 18:15:23 +05:00
parent 9238a7ca66
commit 26b5ea8a91
12 changed files with 116 additions and 29 deletions

View file

@ -126,6 +126,11 @@ class DialogManager : StoreSubscriber<GlobalState> {
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 BackupDialog.AttestationFailed -> AttestationFailedDialog.create(context)
is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context)
is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context)

View file

@ -48,21 +48,33 @@ internal class GeneralUserWalletsListManager(
get() = requireImplementation.isLockable
override val userWallets: Flow<List<UserWallet>>
get() = implementation.transformLatest { impl ->
if (impl != null && impl.hasUserWallets) {
emitAll(impl.userWallets)
get() = implementation
.transformLatest { impl ->
if (impl != null) {
emitAll(impl.userWallets)
}
}
}
// To avoid returning empty flow to subscriber while implementation and userWallets are null
// Flow is called first time when implementation is null and then when its assigned with implementation
// that may have not user wallets (null or empty).
// As a result subscription occurs on empty flow, than will not change if user wallets are available
.filter { requireImplementation.hasUserWallets }
override val userWalletsSync: List<UserWallet>
get() = requireImplementation.userWalletsSync
override val selectedUserWallet: Flow<UserWallet>
get() = implementation.transformLatest { impl ->
if (impl != null && impl.hasUserWallets) {
emitAll(impl.selectedUserWallet)
get() = implementation
.transformLatest { impl ->
if (impl != null) {
emitAll(impl.selectedUserWallet)
}
}
}
// To avoid returning empty flow to subscriber while implementation and userWallets are null
// Flow is called first time when implementation is null and then when its assigned with implementation
// that may have not user wallets (null or empty).
// As a result subscription occurs on empty flow, than will not change if user wallets are available
.filter { requireImplementation.hasUserWallets }
override val selectedUserWalletSync: UserWallet?
get() = requireImplementation.selectedUserWalletSync

View file

@ -46,4 +46,8 @@ internal class WalletConnectEventsHandlerImpl : WalletConnectEventsHandler {
override fun onUnsupportedRequest() {
store.dispatchOnMain(WalletConnectAction.RejectUnsupportedRequest)
}
override fun onPairConnectError(error: Throwable) {
store.dispatchOnMain(WalletConnectAction.PairConnectErrorAction(error))
}
}

View file

@ -246,7 +246,20 @@ internal class DefaultLegacyWalletConnectRepository(
}
override fun pair(uri: String) {
Web3Wallet.pair(Wallet.Params.Pair(uri))
Web3Wallet.pair(
params = Wallet.Params.Pair(uri),
onSuccess = {
Timber.i("Paired successfully: $it")
},
onError = {
Timber.e("Error while pairing: $it")
scope.launch {
_events.emit(
WalletConnectEvents.PairConnectError(it.throwable),
)
}
},
)
}
override fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>) {

View file

@ -14,26 +14,24 @@ 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.DefaultWalletConnectSessionsRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer
import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.scopes.ActivityScoped
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(ActivityComponent::class)
@InstallIn(SingletonComponent::class)
internal object WalletConnectInteractorModule {
@Provides
@ActivityScoped
@Singleton
fun provideWalletConnectInteractor(
wcRepository: LegacyWalletConnectRepository,
wcSessionsRepository: WalletConnectSessionsRepository,
@ -41,6 +39,7 @@ internal object WalletConnectInteractorModule {
currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade,
userWalletsListManager: UserWalletsListManager,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
): WalletConnectInteractor {
return WalletConnectInteractor(
handler = WalletConnectEventsHandlerImpl(),
@ -51,7 +50,7 @@ internal object WalletConnectInteractorModule {
currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade,
userWalletsListManager = userWalletsListManager,
dispatchers = AppCoroutineDispatcherProvider(),
dispatchers = coroutineDispatcherProvider,
)
}
}

View file

@ -16,4 +16,6 @@ interface WalletConnectEventsHandler {
fun onSessionRequest(request: WcPreparedRequest)
fun onUnsupportedRequest()
fun onPairConnectError(error: Throwable)
}

View file

@ -9,18 +9,18 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
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 com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.Stack
@Suppress("LargeClass", "LongParameterList")
class WalletConnectInteractor(
@ -35,18 +35,27 @@ class WalletConnectInteractor(
val blockchainHelper: WcBlockchainHelper,
) {
var isWalletConnectReadyForDeepLinks = false
private val getSelectedWalletUseCase by lazy(LazyThreadSafetyMode.NONE) {
GetSelectedWalletUseCase(userWalletsListManager)
}
private val wcScope = CoroutineScope(
Job() + dispatchers.io + FeatureCoroutineExceptionHandler.create("wcScope"),
SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable ->
Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable")
},
)
private val listenerScope = CoroutineScope(
Job() + dispatchers.io + FeatureCoroutineExceptionHandler.create("listenScope"),
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
@ -96,6 +105,7 @@ class WalletConnectInteractor(
networks = currencies.map { it.network },
)
setUserChains(accounts)
handleDeeplinkStack(accounts)
}
private suspend fun startListeningWc(userWalletId: String, cardId: String?) {
@ -118,6 +128,17 @@ class WalletConnectInteractor(
walletConnectRepository.setUserNamespaces(userNamespaces)
}
private fun handleDeeplinkStack(accounts: List<Account>) {
runCatching {
if (accounts.isEmpty()) return
isWalletConnectReadyForDeepLinks = true
val lastDeeplink = deeplinkStack.pop()
store.dispatchOnMain(WalletConnectAction.OpenSession(lastDeeplink))
}.onFailure {
Timber.e("WC deeplink handling failed. $it")
}
}
private suspend fun subscribeToEvents() {
events
.onEach { wcEvent ->
@ -167,6 +188,9 @@ class WalletConnectInteractor(
is WalletConnectEvents.SessionRequest -> {
handleRequest(wcEvent)
}
is WalletConnectEvents.PairConnectError -> {
handler.onPairConnectError(wcEvent.error)
}
}
}
.flowOn(dispatchers.io)
@ -329,6 +353,21 @@ class WalletConnectInteractor(
return uri.lowercase().startsWith(WC_SCHEME)
}
/**
* 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 (isWalletConnectReadyForDeepLinks) {
store.dispatchOnMain(WalletConnectAction.OpenSession(deeplink))
} else {
deeplinkStack.push(deeplink)
}
}
private fun getCardId(userWallet: UserWallet): String? {
return if (userWallet.scanResponse.card.backupStatus?.isActive != true) {
userWallet.cardId

View file

@ -26,4 +26,6 @@ sealed interface WalletConnectEvents {
val metaUrl: String,
val method: String,
) : WalletConnectEvents
data class PairConnectError(val error: Throwable) : WalletConnectEvents
}

View file

@ -22,17 +22,19 @@ sealed class WalletConnectAction : Action {
data class ShowClipboardOrScanQrDialog(val wcUri: String) : WalletConnectAction()
//region WalletConnect 2.0
object ApproveProposal : WalletConnectAction()
object RejectProposal : WalletConnectAction()
data object ApproveProposal : WalletConnectAction()
data object RejectProposal : WalletConnectAction()
object SessionEstablished : 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()
object RejectUnsupportedRequest : WalletConnectAction()
data object RejectUnsupportedRequest : WalletConnectAction()
data class PerformRequestedAction(val sessionRequest: WcPreparedRequest) : WalletConnectAction()
data class PairConnectErrorAction(val throwable: Throwable) : WalletConnectAction()
//endregion WalletConnect 2.0
}

View file

@ -49,8 +49,11 @@ class WalletConnectMiddleware {
when (action) {
is WalletConnectAction.HandleDeepLink -> {
if (!action.wcUri.isNullOrBlank()) {
store.dispatchOnMain(WalletConnectAction.OpenSession(action.wcUri))
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 -> {
@ -181,6 +184,9 @@ class WalletConnectMiddleware {
is WalletConnectAction.RejectUnsupportedRequest -> {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork()))
}
is WalletConnectAction.PairConnectErrorAction -> {
store.dispatch(GlobalAction.ShowDialog(WalletConnectDialog.PairConnectErrorDialog(action.throwable)))
}
}
}

View file

@ -14,6 +14,7 @@ object WalletConnectReducer {
is WalletConnectAction.RejectProposal,
is WalletConnectAction.SessionEstablished,
is WalletConnectAction.SessionRejected,
is WalletConnectAction.PairConnectErrorAction,
-> state.copy(loading = false)
is WalletConnectAction.SessionListUpdated -> state.copy(
wc2Sessions = action.sessions,

View file

@ -117,6 +117,8 @@ sealed class WalletConnectDialog : StateDialog {
data class SignTransactionDialog(
val data: WcPreparedRequest.SignTransaction,
) : WalletConnectDialog()
data class PairConnectErrorDialog(val error: Throwable) : WalletConnectDialog()
}
data class WcTransactionData(