dev
dart 723 lines 22.7 KB
Raw
1 import 'dart:async';
2 import 'dart:convert';
3 import 'dart:typed_data';
4
5 import 'package:cw_core/utils/print_verbose.dart';
6 import 'package:eth_sig_util/util/utils.dart';
7 import 'package:flutter/material.dart';
8 import 'package:mobx/mobx.dart';
9 import 'package:reown_walletkit/reown_walletkit.dart';
10 import 'package:shared_preferences/shared_preferences.dart';
11
12 import 'package:cake_wallet/.secrets.g.dart' as secrets;
13 import 'package:cake_wallet/entities/preferences_key.dart';
14 import 'package:cake_wallet/generated/i18n.dart';
15 import 'package:cake_wallet/reactions/wallet_connect.dart';
16 import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/eth/evm_chain_id.dart';
17 import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart';
18 import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/chain_key_model.dart';
19 import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
20 import 'package:cake_wallet/src/screens/wallet_connect/utils/eth_utils.dart';
21 import 'package:cake_wallet/src/screens/wallet_connect/utils/method_utils.dart';
22 import 'package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_message_display_widget.dart';
23 import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_connection_request_sheet.dart';
24 import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_signing_request_sheet.dart';
25 import 'package:cake_wallet/store/app_store.dart';
26
27 import 'bottom_sheet_service.dart';
28 import 'chain_service/solana/solana_chain_id.dart';
29 import 'chain_service/solana/solana_chain_service.dart';
30
31 part 'walletkit_service.g.dart';
32
33 class WalletKitService = WalletKitServiceBase with _$WalletKitService;
34
35 abstract class WalletKitServiceBase with Store {
36 WalletKitServiceBase(
37 this._bottomSheetHandler,
38 this.walletKeyService,
39 this.appStore,
40 this.sharedPreferences,
41 ) : pairings = ObservableList<PairingInfo>(),
42 sessions = ObservableList<SessionData>(),
43 auth = ObservableList<PendingSessionAuthRequest>(),
44 isInitialized = false,
45 isLoadingConnections = true;
46
47 final AppStore appStore;
48 final SharedPreferences sharedPreferences;
49 final BottomSheetService _bottomSheetHandler;
50 final WalletConnectKeyService walletKeyService;
51
52 late ReownWalletKit _walletKit;
53
54 @observable
55 bool isInitialized;
56
57 @observable
58 bool isLoadingConnections;
59
60 /// The list of requests from the dapp
61 @observable
62 ObservableList<PairingInfo> pairings;
63
64 @observable
65 ObservableList<SessionData> sessions;
66
67 @observable
68 ObservableList<PendingSessionAuthRequest> auth;
69
70 @action
71 void create() {
72 _walletKit = ReownWalletKit(
73 core: ReownCore(
74 projectId: secrets.walletConnectProjectId,
75 ),
76 metadata: const PairingMetadata(
77 name: 'Cake Wallet',
78 description: 'Cake Wallet',
79 url: 'https://cakewallet.com',
80 icons: ['https://cakewallet.com/assets/image/cake_logo.png'],
81 redirect: Redirect(native: 'cakewallet://'),
82 ),
83 );
84
85 _walletKit.core.addLogListener(_logListener);
86
87 log('Created instance of walletKit');
88
89 _walletKit.core.pairing.onPairingInvalid.subscribe(_onPairingInvalid);
90 _walletKit.core.pairing.onPairingCreate.subscribe(_onPairingCreate);
91 _walletKit.core.relayClient.onRelayClientError.subscribe(_onRelayClientError);
92 _walletKit.core.relayClient.onRelayClientMessage.subscribe(_onRelayClientMessage);
93
94 _walletKit.onSessionProposal.subscribe(_onSessionProposal);
95 _walletKit.onSessionProposalError.subscribe(_onSessionProposalError);
96 _walletKit.onSessionConnect.subscribe(_onSessionConnect);
97
98 if (isEVMCompatibleChain(appStore.wallet!.type)) {
99 _walletKit.onSessionAuthRequest.subscribe(_onSessionAuthRequest);
100 }
101
102 _walletKit.pairings.onSync.subscribe(_onPairingsSync);
103 _walletKit.core.pairing.onPairingDelete.subscribe(_onPairingDelete);
104 _walletKit.core.pairing.onPairingExpire.subscribe(_onPairingDelete);
105
106 // Setup our accounts
107 final chainKeys = walletKeyService.getKeys(appStore.wallet!);
108 for (final chainKey in chainKeys) {
109 for (final chainId in chainKey.chains) {
110 _walletKit.registerAccount(
111 chainId: chainId,
112 accountAddress: chainKey.publicKey,
113 );
114 }
115 }
116 }
117
118 void _logListener(String event) {
119 debugPrint('[WalletKit] $event');
120 }
121
122 @action
123 Future<void> init() async {
124 // Await the initialization of walletKit
125 debugPrint('Intializing walletKit');
126 if (!isInitialized) {
127 try {
128 await _walletKit.init().timeout(
129 const Duration(seconds: 8),
130 onTimeout: () => throw TimeoutException('walletKit init timed out'),
131 );
132 debugPrint('Initialized');
133 isInitialized = true;
134 } catch (e) {
135 debugPrint('init Error: ${e.toString()}');
136 isInitialized = false;
137 }
138 }
139
140 _refreshPairings();
141
142 _reloadSessionsForCurrentWallet();
143
144 auth.clear();
145
146 final newAuthRequests = _walletKit.sessionAuthRequests.getAll();
147 auth.addAll(newAuthRequests);
148
149 isLoadingConnections = false;
150 for (final cId in EVMChainId.values) {
151 EvmChainServiceImpl(
152 reference: cId,
153 appStore: appStore,
154 wcKeyService: walletKeyService,
155 bottomSheetService: _bottomSheetHandler,
156 walletKit: _walletKit,
157 );
158 }
159
160 for (final cId in SolanaChainId.values) {
161 SolanaChainService(
162 reference: cId,
163 appStore: appStore,
164 wcKeyService: walletKeyService,
165 bottomSheetService: _bottomSheetHandler,
166 walletKit: _walletKit,
167 );
168 }
169
170 unawaited(() async {
171 try {
172 await _emitEvent();
173 } catch (e) {
174 printV("emitEvent failed: $e");
175 }
176 }());
177 }
178
179 @action
180 Future<void> _emitEvent({int retries = 0}) async {
181 final isOnline = _walletKit.core.connectivity.isOnline.value;
182 if (!isOnline && retries < 3) {
183 await Future.delayed(const Duration(milliseconds: 500));
184 await _emitEvent(retries: ++retries);
185 return;
186 }
187
188 final engineSessions = _walletKit.sessions.getAll();
189 for (var session in engineSessions) {
190 final chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
191 for (var chain in chainKeys) {
192 if (!MethodsUtils.isSessionOwnedByWallet(session, chain.publicKey)) {
193 continue;
194 }
195
196 for (var chainID in chain.chains) {
197 try {
198 final events = NamespaceUtils.getNamespacesEventsForChain(
199 chainId: chainID,
200 namespaces: session.namespaces,
201 );
202 if (events.contains('accountsChanged')) {
203 await _walletKit
204 .emitSessionEvent(
205 topic: session.topic,
206 chainId: chainID,
207 event: SessionEventParams(
208 name: 'accountsChanged',
209 data: [chain.publicKey],
210 ),
211 )
212 .timeout(const Duration(seconds: 3));
213 }
214 } on ReownSignError catch (e) {
215 if (e.code == 6) {
216 try {
217 await deletePairing(topic: session.pairingTopic);
218 } catch (_) {}
219 sessions.removeWhere((s) => s.topic == session.topic);
220 _refreshPairings();
221 }
222 } catch (_) {}
223 }
224 }
225 }
226 }
227
228 @action
229 void resetConnectionsState() {
230 sessions.clear();
231 auth.clear();
232 pairings.clear();
233
234 isInitialized = false;
235 isLoadingConnections = true;
236 }
237
238 @action
239 Future<void> onDispose() async {
240 log("walletKit dispose");
241
242 resetConnectionsState();
243
244 _walletKit.core.removeLogListener(_logListener);
245
246 _walletKit.core.pairing.onPairingInvalid.unsubscribe(_onPairingInvalid);
247 _walletKit.core.pairing.onPairingCreate.unsubscribe(_onPairingCreate);
248 _walletKit.core.relayClient.onRelayClientError.unsubscribe(_onRelayClientError);
249 _walletKit.core.relayClient.onRelayClientMessage.unsubscribe(_onRelayClientMessage);
250
251 _walletKit.onSessionProposal.unsubscribe(_onSessionProposal);
252 _walletKit.onSessionProposalError.unsubscribe(_onSessionProposalError);
253 _walletKit.onSessionConnect.unsubscribe(_onSessionConnect);
254 _walletKit.onSessionAuthRequest.unsubscribe(_onSessionAuthRequest);
255
256 _walletKit.pairings.onSync.unsubscribe(_onPairingsSync);
257 _walletKit.core.pairing.onPairingDelete.unsubscribe(_onPairingDelete);
258 _walletKit.core.pairing.onPairingExpire.unsubscribe(_onPairingDelete);
259
260 try {
261 await _walletKit.core.relayClient.disconnect().timeout(const Duration(seconds: 3));
262 } catch (e) {
263 printV("walletKit relay disconnect: $e");
264 }
265 }
266
267 ReownWalletKit get walletKit => _walletKit;
268
269 void _onRelayClientMessage(MessageEvent? event) async {
270 if (event != null) {
271 final jsonObject = await EthUtils.decodeMessageEvent(event);
272 debugPrint('_onRelayClientMessage $jsonObject');
273
274 if (jsonObject is JsonRpcRequest) {
275 debugPrint(jsonObject.id.toString());
276 debugPrint(jsonObject.method);
277
278 if (jsonObject.method == 'wc_sessionDelete') {
279 await disconnectSession(topic: event.topic);
280 }
281 }
282 }
283 }
284
285 void _onPairingsSync(StoreSyncEvent? args) {
286 if (args != null) {
287 _refreshPairings();
288 }
289 }
290
291 void _onPairingDelete(PairingEvent? event) {
292 _refreshPairings();
293 }
294
295 @action
296 Future<void> _onSessionProposal(SessionProposalEvent? args) async {
297 debugPrint('_onSessionProposal ${jsonEncode(args?.params)}');
298
299 if (args != null) {
300 final proposer = args.params.proposer;
301 final result = (await _bottomSheetHandler.queueBottomSheet(
302 widget: WCConnectionRequestSheet(
303 proposalData: args.params,
304 requester: proposer,
305 verifyContext: args.verifyContext,
306 walletKeyService: walletKeyService,
307 appStore: appStore,
308 ),
309 )) ??
310 WCBottomSheetResult.reject;
311
312 if (result != WCBottomSheetResult.reject) {
313 try {
314 await _walletKit.approveSession(
315 id: args.id,
316 namespaces: NamespaceUtils.regenerateNamespacesWithChains(
317 args.params.generatedNamespaces!,
318 ),
319 sessionProperties: args.params.sessionProperties,
320 );
321 } on ReownSignError catch (error) {
322 MethodsUtils.handleRedirect(
323 '',
324 proposer.metadata.redirect,
325 error.message,
326 );
327 }
328 } else {
329 final error = Errors.getSdkError(Errors.USER_REJECTED).toSignError();
330 await _walletKit.rejectSession(id: args.id, reason: error);
331 await _walletKit.core.pairing.disconnect(topic: args.params.pairingTopic);
332 MethodsUtils.handleRedirect(
333 '',
334 proposer.metadata.redirect,
335 error.message,
336 );
337 }
338 }
339 }
340
341 @action
342 Future<void> _onSessionProposalError(SessionProposalErrorEvent? args) async {
343 debugPrint('_onSessionProposalError $args');
344
345 if (args != null) {
346 String errorMessage = args.error.message;
347 if (args.error.code == 5100) {
348 errorMessage =
349 errorMessage.replaceFirst('${S.current.requested}:', '\n\n${S.current.requested}:');
350 errorMessage =
351 errorMessage.replaceFirst('${S.current.supported}:', '\n\n${S.current.supported}:');
352 }
353 MethodsUtils.goBackModal(
354 title: S.current.error,
355 message: errorMessage,
356 success: false,
357 );
358 }
359 }
360
361 @action
362 Future<void> _onSessionConnect(SessionConnect? args) async {
363 if (args != null) {
364 final session = jsonEncode(args.session.toJson());
365
366 debugPrint('_onSessionConnect $session');
367
368 await savePairingTopicToLocalStorage(args.session.pairingTopic);
369
370 sessions.add(args.session);
371
372 _refreshPairings();
373
374 MethodsUtils.handleRedirect(
375 args.session.topic,
376 args.session.peer.metadata.redirect,
377 '',
378 true,
379 );
380 }
381 }
382
383 @action
384 void _onRelayClientError(ErrorEvent? args) {
385 debugPrint('_onRelayClientError ${args?.error}');
386 // _bottomSheetHandler.queueBottomSheet(
387 // isModalDismissible: true,
388 // widget: BottomSheetMessageDisplayWidget(
389 // message: "WC RelayClient Error: ${args?.error}",
390 // ),
391 // );
392 }
393
394 @action
395 void _onPairingInvalid(PairingInvalidEvent? args) {
396 debugPrint('_onPairingInvalid $args');
397 _bottomSheetHandler.queueBottomSheet(
398 isModalDismissible: true,
399 widget: BottomSheetMessageDisplayWidget(
400 message: '${S.current.pairingInvalidEvent}: $args',
401 ),
402 );
403 }
404
405 @action
406 void _onPairingCreate(PairingEvent? args) {
407 debugPrint('_onPairingCreate $args');
408
409 if (args != null && args.topic != null && args.topic!.isNotEmpty) {
410 savePairingTopicToLocalStorage(args.topic!);
411
412 _refreshPairings();
413 }
414 }
415
416 Future<void> _onSessionAuthRequest(SessionAuthRequest? args) async {
417 if (args != null) {
418 final SessionAuthPayload authPayload = args.authPayload;
419 final jsonPyaload = jsonEncode(authPayload.toJson());
420
421 debugPrint('_onSessionAuthRequest $jsonPyaload');
422
423 final chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
424 final supportedChains = chainKeys.first.chains;
425 final supportedMethods = getChainSupportedMethodsOnWalletType(appStore.wallet!.type);
426
427 final newAuthPayload = AuthSignature.populateAuthPayload(
428 authPayload: authPayload,
429 chains: supportedChains.toList(),
430 methods: supportedMethods.toList(),
431 );
432 final cacaoRequestPayload = CacaoRequestPayload.fromSessionAuthPayload(
433 newAuthPayload,
434 );
435
436 final List<Map<String, dynamic>> formattedMessages = [];
437 for (var chain in newAuthPayload.chains) {
438 final chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
439 final iss = 'did:pkh:$chain:${chainKeys.first.publicKey}';
440
441 final message = _walletKit.formatAuthMessage(
442 iss: iss,
443 cacaoPayload: cacaoRequestPayload,
444 );
445 formattedMessages.add({iss: message});
446 }
447
448 final requesterMetadata = args.requester.metadata;
449 final requesterIcon =
450 requesterMetadata.icons.isNotEmpty ? requesterMetadata.icons.first : null;
451 final chainKeysForAuth = walletKeyService.getKeysForChain(appStore.wallet!);
452 final addressForAuth = chainKeysForAuth.isNotEmpty ? chainKeysForAuth.first.publicKey : '';
453 final combinedMessageBody =
454 formattedMessages.map((m) => m.values.first as String).join('\n\n');
455
456 final WCBottomSheetResult result = (await _bottomSheetHandler.queueBottomSheet(
457 widget: WCSigningRequestSheet(
458 title: S.current.wc_signing_request_title,
459 swipeLabel: S.current.wc_swipe_to_sign,
460 dappName: requesterMetadata.name,
461 dappIconUrl: requesterIcon,
462 dappSubtitle: requesterMetadata.url,
463 message: combinedMessageBody,
464 walletName: appStore.wallet?.name ?? '',
465 address: addressForAuth,
466 verifyContext: args.verifyContext,
467 signAllCount: formattedMessages.length,
468 ),
469 ) as WCBottomSheetResult?) ??
470 WCBottomSheetResult.reject;
471
472 if (result != WCBottomSheetResult.reject) {
473 final chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
474 final privateKey = '0x${chainKeys.first.privateKey}';
475 final credentials = EthPrivateKey.fromHex(privateKey);
476 final messageToSign = formattedMessages.length;
477 final count = (result == WCBottomSheetResult.one) ? 1 : messageToSign;
478 final List<Cacao> cacaos = [];
479 for (var i = 0; i < count; i++) {
480 final iss = formattedMessages[i].keys.first;
481 final message = formattedMessages[i].values.first as String;
482
483 final signature = credentials.signPersonalMessageToUint8List(
484 Uint8List.fromList(message.codeUnits),
485 );
486 final hexSignature = bytesToHex(signature, include0x: true);
487
488 cacaos.add(
489 AuthSignature.buildAuthObject(
490 requestPayload: cacaoRequestPayload,
491 signature: CacaoSignature(t: CacaoSignature.EIP191, s: hexSignature),
492 iss: iss,
493 ),
494 );
495 }
496 //
497 try {
498 final session = await _walletKit.approveSessionAuthenticate(
499 id: args.id,
500 auths: cacaos,
501 );
502
503 debugPrint('_onSessionAuthRequest - approveSessionAuthenticate $session');
504
505 MethodsUtils.handleRedirect(
506 session.topic,
507 session.session?.peer.metadata.redirect,
508 '',
509 true,
510 );
511 } on ReownSignError catch (error) {
512 MethodsUtils.handleRedirect(
513 args.topic,
514 args.requester.metadata.redirect,
515 error.message,
516 );
517 }
518 } else {
519 final error = Errors.getSdkError(Errors.USER_REJECTED_AUTH);
520 await _walletKit.rejectSessionAuthenticate(
521 id: args.id,
522 reason: error.toSignError(),
523 );
524 MethodsUtils.handleRedirect(
525 args.topic,
526 args.requester.metadata.redirect,
527 error.message,
528 );
529 }
530 }
531 }
532
533 @action
534 Future<void> deletePairing({required String topic}) async {
535 final topicSessions = sessions.where((element) => element.pairingTopic == topic).toList();
536
537 await _walletKit.core.pairing.disconnect(topic: topic);
538 for (var session in topicSessions) {
539 try {
540 await _walletKit.disconnectSession(
541 topic: session.topic,
542 reason: Errors.getSdkError(Errors.USER_DISCONNECTED).toSignError(),
543 );
544 } catch (_) {}
545 }
546
547 await _removePairingTopicFromLocalStorage(topic);
548 _reloadSessionsForCurrentWallet();
549 _refreshPairings();
550 }
551
552 @action
553 Future<void> disconnectSession({required String topic}) async {
554 String? pairingTopic;
555 for (final s in sessions) {
556 if (s.topic == topic) {
557 pairingTopic = s.pairingTopic;
558 break;
559 }
560 }
561 pairingTopic ??= _walletKit.sessions.get(topic)?.pairingTopic;
562
563 try {
564 await walletKit.disconnectSession(
565 topic: topic,
566 reason: Errors.getSdkError(Errors.USER_DISCONNECTED).toSignError(),
567 );
568 } catch (e) {
569 printV('disconnectSession: $e');
570 }
571
572 _reloadSessionsForCurrentWallet();
573
574 if (pairingTopic != null &&
575 !_walletKit.sessions.getAll().any((s) => s.pairingTopic == pairingTopic)) {
576 await _removePairingTopicFromLocalStorage(pairingTopic);
577 try {
578 await _walletKit.core.pairing.disconnect(topic: pairingTopic);
579 } catch (_) {}
580 }
581
582 _refreshPairings();
583 }
584
585 @action
586 Future<void> pairWithUri(Uri uri) async {
587 try {
588 debugPrint('pairWithUri - Pairing with URI: $uri');
589 await _walletKit.pair(uri: uri);
590 } on ReownSignError catch (e) {
591 _bottomSheetHandler.queueBottomSheet(
592 isModalDismissible: true,
593 widget: BottomSheetMessageDisplayWidget(message: e.message),
594 );
595 } catch (e) {
596 _bottomSheetHandler.queueBottomSheet(
597 isModalDismissible: true,
598 widget: BottomSheetMessageDisplayWidget(message: e.toString()),
599 );
600 }
601 }
602
603 @action
604 void _refreshPairings() {
605 debugPrint('_refreshPairings - Refreshing pairings');
606
607 pairings.clear();
608
609 final allPairings = _walletKit.pairings.getAll();
610
611 final keyForWallet = getKeyForStoringTopicsForWallet();
612
613 if (keyForWallet.isEmpty) return;
614
615 final currentTopicsForWallet = getPairingTopicsForWallet(keyForWallet);
616
617 final filteredPairings = allPairings.where(
618 (pairing) {
619 bool isInCurrentTopics = currentTopicsForWallet.contains(pairing.topic);
620 // bool isActive = pairing.active;
621 // bool hasSession = sessions.any((session) => session.pairingTopic == pairing.topic);
622
623 // return isInCurrentTopics && isActive;
624 return isInCurrentTopics;
625 },
626 ).toList();
627
628 pairings.addAll(filteredPairings);
629 }
630
631 @action
632 void _reloadSessionsForCurrentWallet() {
633 sessions.clear();
634
635 final chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
636 if (chainKeys.isEmpty) {
637 return;
638 }
639
640 final walletPublicKey = chainKeys.first.publicKey;
641
642 sessions.addAll(
643 _walletKit.sessions
644 .getAll()
645 .where((session) => MethodsUtils.isSessionOwnedByWallet(session, walletPublicKey)),
646 );
647 }
648
649 @action
650 List<SessionData> getSessionsForPairingInfo(PairingInfo pairing) {
651 return sessions.where((element) => element.pairingTopic == pairing.topic).toList();
652 }
653
654 String getKeyForStoringTopicsForWallet() {
655 try {
656 // For EVM wallets, use getKeys() to get all EVM keys
657 // since the same address works across all EVM chains. For non-EVM wallets, use getKeysForChain()
658 // to get keys specific to the current chain.
659 List<ChainKeyModel> chainKeys;
660 if (isEVMCompatibleChain(appStore.wallet!.type)) {
661 chainKeys = walletKeyService.getKeys(appStore.wallet!);
662
663 chainKeys = chainKeys
664 .where((key) => key.chains.any((chain) => chain.startsWith('eip155:')))
665 .toList();
666 } else {
667 chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
668 }
669
670 if (chainKeys.isEmpty) return '';
671
672 final publicKey = chainKeys.first.publicKey;
673 if (publicKey.isEmpty) return '';
674
675 return PreferencesKey.walletConnectPairingTopicsListForWallet(publicKey);
676 } catch (e) {
677 return '';
678 }
679 }
680
681 List<String> getPairingTopicsForWallet(String key) {
682 final jsonString = sharedPreferences.getString(key);
683
684 if (jsonString == null) {
685 return [];
686 }
687
688 final List<dynamic> jsonList = jsonDecode(jsonString) as List<dynamic>;
689
690 return jsonList.map((item) => item as String).toList();
691 }
692
693 Future<void> _removePairingTopicFromLocalStorage(String pairingTopic) async {
694 final key = getKeyForStoringTopicsForWallet();
695 if (key.isEmpty) return;
696
697 final topics = getPairingTopicsForWallet(key);
698 if (!topics.contains(pairingTopic)) return;
699
700 topics.remove(pairingTopic);
701 await sharedPreferences.setString(key, jsonEncode(topics));
702 }
703
704 Future<void> savePairingTopicToLocalStorage(String pairingTopic) async {
705 final key = getKeyForStoringTopicsForWallet();
706
707 if (key.isEmpty) return;
708
709 final pairingTopicsForWallet = getPairingTopicsForWallet(key);
710
711 bool isPairingTopicAlreadySaved = pairingTopicsForWallet.contains(pairingTopic);
712 debugPrint(
713 'Is Pairing Topic Saved: $isPairingTopicAlreadySaved, Key: $key, Topic: $pairingTopic');
714
715 if (!isPairingTopicAlreadySaved) {
716 pairingTopicsForWallet.add(pairingTopic);
717
718 final jsonString = jsonEncode(pairingTopicsForWallet);
719
720 await sharedPreferences.setString(key, jsonString);
721 }
722 }
723 }