fix: Better handle expired session and auth data for walletconnect (#3228)
What changed, and why it matters
This commit fixes how Cake Wallet's WalletConnect feature handles expired or disconnected sessions. Previously, the app's local list of sessions and authentication data could become out of sync with the actual WalletConnect state, potentially leaving stale connection data around or showing outdated session information. The patch clears and refreshes these lists more reliably when sessions end, expire, or pairings are deleted, and adds retry logic for network-related event emission.
Review whether stale session/auth data could have led to any security-relevant behavior (e.g., reusing session topics, displaying wrong connected dApps, or missing disconnect notifications). Consider whether additional cleanup is needed for `auth` requests tied to deleted pairings. No immediate exploit is evident from the diff alone.
Security signals we found
State desynchronization between local MobX stores and WalletConnect engine state
Stale pairing topics persisted in local storage after session disconnection
Missing error handling in disconnect/delete flows could leave inconsistent session data
Retry logic added for network-dependent event emission to reduce missed session updates
Evidence from the diff
The change modifies WalletKitService to better synchronize local MobX stores (sessions, auth, pairings) with the underlying WalletConnect walletkit engine. Key changes: clearing sessions/auth before repopulating from _walletKit.sessions.getAll(); adding _removePairingTopicFromLocalStorage() to prune persisted pairing topics; updating deletePairing() and disconnectSession() to refresh local state and clean up local storage; adding retry logic with backoff to _emitEvent(); and removing duplicate chainKeys lookups in the event emission loop. The patch also wraps several engine calls in try/catch to prevent exceptions from leaving local state inconsistent.
Changed components
lib/src/screens/wallet_connect/services/walletkit_service.dartWalletConnect session managementLocal pairing topic persistence (SharedPreferences)Inspect captured patch +72 / −15
diff --git a/lib/src/screens/wallet_connect/services/walletkit_service.dart b/lib/src/screens/wallet_connect/services/walletkit_service.dart
index fad06532..48d10f51 100644
--- a/lib/src/screens/wallet_connect/services/walletkit_service.dart
+++ b/lib/src/screens/wallet_connect/services/walletkit_service.dart
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
+import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:eth_sig_util/util/utils.dart';
import 'package:flutter/material.dart';
@@ -149,6 +150,9 @@ abstract class WalletKitServiceBase with Store {
_refreshPairings();
+ sessions.clear();
+ auth.clear();
+
final newSessions = _walletKit.sessions.getAll();
sessions.addAll(newSessions);
@@ -185,12 +189,16 @@ abstract class WalletKitServiceBase with Store {
final isOnline = _walletKit.core.connectivity.isOnline.value;
if (!isOnline) {
await Future.delayed(const Duration(milliseconds: 500));
- _emitEvent();
+Future<void> _emitEvent({int retries = 0}) async {
+final isOnline = _walletKit.core.connectivity.isOnline.value;
+if (!isOnline && retries < 3) {
+await Future.delayed(const Duration(milliseconds: 500));
+ await _emitEvent(retries: ++retries);
return;
}
- final sessions = _walletKit.sessions.getAll();
- for (var session in sessions) {
+ final engineSessions = _walletKit.sessions.getAll();
+ for (var session in engineSessions) {
final chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
for (var chain in chainKeys) {
for (var chainID in chain.chains) {
@@ -200,13 +208,12 @@ abstract class WalletKitServiceBase with Store {
namespaces: session.namespaces,
);
if (events.contains('accountsChanged')) {
- final chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
- _walletKit.emitSessionEvent(
+ await _walletKit.emitSessionEvent(
topic: session.topic,
chainId: chainID,
event: SessionEventParams(
name: 'accountsChanged',
- data: [chainKeys.first.publicKey],
+ data: [chain.publicKey],
),
);
}
@@ -215,6 +222,7 @@ abstract class WalletKitServiceBase with Store {
try {
await deletePairing(topic: session.pairingTopic);
} catch (_) {}
+ sessions.removeWhere((s) => s.topic == session.topic);
_refreshPairings();
}
} catch (_) {}
@@ -242,6 +250,10 @@ abstract class WalletKitServiceBase with Store {
_walletKit.core.pairing.onPairingDelete.unsubscribe(_onPairingDelete);
_walletKit.core.pairing.onPairingExpire.unsubscribe(_onPairingDelete);
+ sessions.clear();
+ auth.clear();
+ pairings.clear();
+
isInitialized = false;
}
@@ -511,23 +523,57 @@ abstract class WalletKitServiceBase with Store {
@action
Future<void> deletePairing({required String topic}) async {
- final topicSessions = sessions.where((element) => element.pairingTopic == topic);
+ final topicSessions =
+ sessions.where((element) => element.pairingTopic == topic).toList();
await _walletKit.core.pairing.disconnect(topic: topic);
for (var session in topicSessions) {
- await _walletKit.disconnectSession(
- topic: session.topic,
- reason: Errors.getSdkError(Errors.USER_DISCONNECTED).toSignError(),
- );
+ try {
+ await _walletKit.disconnectSession(
+ topic: session.topic,
+ reason: Errors.getSdkError(Errors.USER_DISCONNECTED).toSignError(),
+ );
+ } catch (_) {}
}
+
+ await _removePairingTopicFromLocalStorage(topic);
+ sessions.clear();
+ sessions.addAll(_walletKit.sessions.getAll());
+ _refreshPairings();
}
@action
Future<void> disconnectSession({required String topic}) async {
- await walletKit.disconnectSession(
- topic: topic,
- reason: Errors.getSdkError(Errors.USER_DISCONNECTED).toSignError(),
- );
+ String? pairingTopic;
+ for (final s in sessions) {
+ if (s.topic == topic) {
+ pairingTopic = s.pairingTopic;
+ break;
+ }
+ }
+ pairingTopic ??= _walletKit.sessions.get(topic)?.pairingTopic;
+
+ try {
+ await walletKit.disconnectSession(
+ topic: topic,
+ reason: Errors.getSdkError(Errors.USER_DISCONNECTED).toSignError(),
+ );
+ } catch (e) {
+ printV('disconnectSession: $e');
+ }
+
+ sessions.clear();
+ sessions.addAll(_walletKit.sessions.getAll());
+
+ if (pairingTopic != null &&
+ !_walletKit.sessions.getAll().any((s) => s.pairingTopic == pairingTopic)) {
+ await _removePairingTopicFromLocalStorage(pairingTopic);
+ try {
+ await _walletKit.core.pairing.disconnect(topic: pairingTopic);
+ } catch (_) {}
+ }
+
+ _refreshPairings();
}
@action
@@ -637,6 +683,17 @@ abstract class WalletKitServiceBase with Store {
return jsonList.map((item) => item as String).toList();
}
+ Future<void> _removePairingTopicFromLocalStorage(String pairingTopic) async {
+ final key = getKeyForStoringTopicsForWallet();
+ if (key.isEmpty) return;
+
+ final topics = getPairingTopicsForWallet(key);
+ if (!topics.contains(pairingTopic)) return;
+
+ topics.remove(pairingTopic);
+ await sharedPreferences.setString(key, jsonEncode(topics));
+ }
+
Future<void> savePairingTopicToLocalStorage(String pairingTopic) async {
// Get key specific to the current wallet
final key = getKeyForStoringTopicsForWallet();
Why this scored 34/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.