diff --git a/android/app/build.gradle b/android/app/build.gradle
index 6c299c929..4a8045bb3 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -42,6 +42,14 @@ android {
disable 'InvalidPackage'
}
+ compileOptions {
+ coreLibraryDesugaringEnabled true
+
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
+
namespace "com.cakewallet.cake_wallet"
defaultConfig {
@@ -73,7 +81,6 @@ android {
buildTypes {
release {
signingConfig signingConfigs.release
-
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
@@ -92,6 +99,7 @@ dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
}
configurations {
implementation.exclude module:'proto-google-common-protos'
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
index 921ee4d4c..a733bae9e 100644
--- a/android/app/proguard-rules.pro
+++ b/android/app/proguard-rules.pro
@@ -6,4 +6,97 @@
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
-dontwarn io.flutter.embedding.**
--dontwarn com.google.android.play.core.splitcompat.SplitCompatApplication
\ No newline at end of file
+-dontwarn com.google.android.play.core.splitcompat.SplitCompatApplication
+
+# start reown
+-dontwarn com.github.luben.zstd.BufferPool
+-dontwarn com.github.luben.zstd.ZstdInputStream
+-dontwarn com.github.luben.zstd.ZstdOutputStream
+-dontwarn com.google.api.client.http.GenericUrl
+-dontwarn com.google.api.client.http.HttpHeaders
+-dontwarn com.google.api.client.http.HttpRequest
+-dontwarn com.google.api.client.http.HttpRequestFactory
+-dontwarn com.google.api.client.http.HttpResponse
+-dontwarn com.google.api.client.http.HttpTransport
+-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder
+-dontwarn com.google.api.client.http.javanet.NetHttpTransport
+-dontwarn java.awt.Color
+-dontwarn java.awt.Dimension
+-dontwarn java.awt.Graphics2D
+-dontwarn java.awt.Graphics
+-dontwarn java.awt.Image
+-dontwarn java.awt.Point
+-dontwarn java.awt.Polygon
+-dontwarn java.awt.Shape
+-dontwarn java.awt.color.ColorSpace
+-dontwarn java.awt.geom.AffineTransform
+-dontwarn java.awt.image.BufferedImage
+-dontwarn java.awt.image.ColorModel
+-dontwarn java.awt.image.ComponentColorModel
+-dontwarn java.awt.image.ComponentSampleModel
+-dontwarn java.awt.image.DataBuffer
+-dontwarn java.awt.image.DataBufferByte
+-dontwarn java.awt.image.DataBufferInt
+-dontwarn java.awt.image.DataBufferUShort
+-dontwarn java.awt.image.ImageObserver
+-dontwarn java.awt.image.MultiPixelPackedSampleModel
+-dontwarn java.awt.image.Raster
+-dontwarn java.awt.image.RenderedImage
+-dontwarn java.awt.image.SampleModel
+-dontwarn java.awt.image.SinglePixelPackedSampleModel
+-dontwarn java.awt.image.WritableRaster
+-dontwarn java.beans.BeanInfo
+-dontwarn java.beans.FeatureDescriptor
+-dontwarn java.beans.IntrospectionException
+-dontwarn java.beans.Introspector
+-dontwarn java.beans.PropertyDescriptor
+-dontwarn java.lang.reflect.InaccessibleObjectException
+-dontwarn javax.imageio.IIOImage
+-dontwarn javax.imageio.ImageIO
+-dontwarn javax.imageio.ImageWriteParam
+-dontwarn javax.imageio.ImageWriter
+-dontwarn javax.imageio.metadata.IIOMetadata
+-dontwarn javax.imageio.stream.ImageOutputStream
+-dontwarn javax.swing.JComponent
+-dontwarn javax.swing.JFileChooser
+-dontwarn javax.swing.JFrame
+-dontwarn javax.swing.JPanel
+-dontwarn javax.swing.ProgressMonitor
+-dontwarn javax.swing.SwingUtilities
+-dontwarn org.brotli.dec.BrotliInputStream
+-dontwarn org.joda.time.Instant
+-dontwarn org.objectweb.asm.AnnotationVisitor
+-dontwarn org.objectweb.asm.Attribute
+-dontwarn org.objectweb.asm.ClassReader
+-dontwarn org.objectweb.asm.ClassVisitor
+-dontwarn org.objectweb.asm.FieldVisitor
+-dontwarn org.objectweb.asm.Label
+-dontwarn org.objectweb.asm.MethodVisitor
+-dontwarn org.objectweb.asm.Type
+-dontwarn org.tukaani.xz.ARMOptions
+-dontwarn org.tukaani.xz.ARMThumbOptions
+-dontwarn org.tukaani.xz.DeltaOptions
+-dontwarn org.tukaani.xz.FilterOptions
+-dontwarn org.tukaani.xz.FinishableOutputStream
+-dontwarn org.tukaani.xz.FinishableWrapperOutputStream
+-dontwarn org.tukaani.xz.IA64Options
+-dontwarn org.tukaani.xz.LZMA2InputStream
+-dontwarn org.tukaani.xz.LZMA2Options
+-dontwarn org.tukaani.xz.LZMAInputStream
+-dontwarn org.tukaani.xz.LZMAOutputStream
+-dontwarn org.tukaani.xz.MemoryLimitException
+-dontwarn org.tukaani.xz.PowerPCOptions
+-dontwarn org.tukaani.xz.SPARCOptions
+-dontwarn org.tukaani.xz.SingleXZInputStream
+-dontwarn org.tukaani.xz.UnsupportedOptionsException
+-dontwarn org.tukaani.xz.X86Options
+-dontwarn org.tukaani.xz.XZ
+-dontwarn org.tukaani.xz.XZInputStream
+-dontwarn org.tukaani.xz.XZOutputStream
+-dontwarn us.hebi.matlab.mat.ejml.Mat5Ejml
+-dontwarn us.hebi.matlab.mat.format.Mat5
+-dontwarn us.hebi.matlab.mat.format.Mat5File
+-dontwarn us.hebi.matlab.mat.types.Array
+-dontwarn us.hebi.matlab.mat.types.MatFile$Entry
+-dontwarn us.hebi.matlab.mat.types.MatFile
+# end reown
\ No newline at end of file
diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml
index 4f15370c3..280a45b3c 100644
--- a/android/app/src/main/AndroidManifestBase.xml
+++ b/android/app/src/main/AndroidManifestBase.xml
@@ -24,6 +24,10 @@
+
+
+
+
+
stopSync() async {
if (isBackgroundSyncRunning) {
printV("Stopping background sync");
- await save();
+ monero.Wallet_store(wptr!);
monero.Wallet_stopBackgroundSync(wptr!, '');
+ monero_wallet.store();
isBackgroundSyncRunning = false;
}
await save();
@@ -268,9 +269,9 @@ abstract class MoneroWalletBase extends WalletBase stopBackgroundSync(String password) async {
if (isBackgroundSyncRunning) {
printV("Stopping background sync");
- await save();
+ monero.Wallet_store(wptr!);
monero.Wallet_stopBackgroundSync(wptr!, password);
- await save();
+ monero.Wallet_store(wptr!);
isBackgroundSyncRunning = false;
}
}
diff --git a/cw_shared_external/android/build.gradle b/cw_shared_external/android/build.gradle
index 8d2b1b13d..360f518ff 100644
--- a/cw_shared_external/android/build.gradle
+++ b/cw_shared_external/android/build.gradle
@@ -9,7 +9,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:7.3.0'
+ classpath 'com.android.tools.build:gradle:8.7.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart
index fc14dbbbd..40eecc25a 100644
--- a/cw_solana/lib/solana_client.dart
+++ b/cw_solana/lib/solana_client.dart
@@ -171,37 +171,85 @@ class SolanaWalletClient {
if (programId == SystemProgramConst.programId) {
// For native solana transactions
- if (instruction.accounts.length < 2) continue;
- final senderIndex = instruction.accounts[0];
- final receiverIndex = instruction.accounts[1];
- sender = message.accountKeys[senderIndex].address;
- receiver = message.accountKeys[receiverIndex].address;
+ if (txResponse.version == TransactionType.legacy) {
+ // For legacy transfers, the fee payer (index 0) is the sender.
+ sender = message.accountKeys[0].address;
- final feeForTx = fee / SolanaUtils.lamportsPerSol;
+ final senderPreBalance = meta.preBalances[0];
+ final senderPostBalance = meta.postBalances[0];
+ final feeForTx = fee / SolanaUtils.lamportsPerSol;
- final preBalances = meta.preBalances;
- final postBalances = meta.postBalances;
+ // The loss on the sender's account would include both the transfer amount and the fee.
+ // So we would subtract the fee to calculate the actual amount that was transferred (in lamports).
+ final transferLamports = (senderPreBalance - senderPostBalance) - BigInt.from(fee);
- final amountInString =
- (((preBalances[senderIndex] - postBalances[senderIndex]) / BigInt.from(1e9))
- .toDouble() -
- feeForTx)
- .toStringAsFixed(6);
+ // Next, we attempt to find the receiver by comparing the balance changes.
+ // (The index 0 is for the sender so we skip it.)
+ bool foundReceiver = false;
+ for (int i = 1; i < meta.preBalances.length; i++) {
+ // The increase in balance on the receiver account should correspond to the transfer amount we calculated earlieer.
+ final pre = meta.preBalances[i];
+ final post = meta.postBalances[i];
+ if ((post - pre) == transferLamports) {
+ receiver = message.accountKeys[i].address;
+ foundReceiver = true;
+ break;
+ }
+ }
- final amount = double.parse(amountInString);
+ if (!foundReceiver) {
+ // Optionally (and rarely), if no account shows the exact expected change,
+ // we set the receiver address to unknown.
+ receiver = "unknown";
+ }
- return SolanaTransactionModel(
- isOutgoingTx: sender == walletAddress,
- from: sender,
- to: receiver,
- id: signature,
- amount: amount.abs(),
- programId: SystemProgramConst.programId.address,
- tokenSymbol: 'SOL',
- blockTimeInInt: blockTime?.toInt() ?? 0,
- fee: feeForTx,
- );
+ final amount = transferLamports / BigInt.from(1e9);
+
+ return SolanaTransactionModel(
+ isOutgoingTx: sender == walletAddress,
+ from: sender,
+ to: receiver,
+ id: signature,
+ amount: amount.abs(),
+ programId: SystemProgramConst.programId.address,
+ tokenSymbol: 'SOL',
+ blockTimeInInt: blockTime?.toInt() ?? 0,
+ fee: feeForTx,
+ );
+ } else {
+ if (instruction.accounts.length < 2) continue;
+ final senderIndex = instruction.accounts[0];
+ final receiverIndex = instruction.accounts[1];
+
+ sender = message.accountKeys[senderIndex].address;
+ receiver = message.accountKeys[receiverIndex].address;
+
+ final feeForTx = fee / SolanaUtils.lamportsPerSol;
+
+ final preBalances = meta.preBalances;
+ final postBalances = meta.postBalances;
+
+ final amountInString =
+ (((preBalances[senderIndex] - postBalances[senderIndex]) / BigInt.from(1e9))
+ .toDouble() -
+ feeForTx)
+ .toStringAsFixed(6);
+
+ final amount = double.parse(amountInString);
+
+ return SolanaTransactionModel(
+ isOutgoingTx: sender == walletAddress,
+ from: sender,
+ to: receiver,
+ id: signature,
+ amount: amount.abs(),
+ programId: SystemProgramConst.programId.address,
+ tokenSymbol: 'SOL',
+ blockTimeInInt: blockTime?.toInt() ?? 0,
+ fee: feeForTx,
+ );
+ }
} else if (programId == SPLTokenProgramConst.tokenProgramId) {
// For SPL Token transactions
if (instruction.accounts.length < 2) continue;
@@ -842,6 +890,7 @@ class SolanaWalletClient {
}) async {
/// Sign the transaction with the owner's private key.
final ownerSignature = ownerPrivateKey.sign(transaction.serializeMessage());
+
transaction.addSignature(ownerPrivateKey.publicKey().toAddress(), ownerSignature);
/// Serialize the transaction.
diff --git a/ios/Podfile b/ios/Podfile
index f0a0721a6..cf24cb605 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
-platform :ios, '12.0'
+platform :ios, '13.0'
source 'https://github.com/CocoaPods/Specs.git'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
@@ -43,7 +43,7 @@ post_install do |installer|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
- config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
+ config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index d9ff6b66e..794305883 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -501,7 +501,7 @@
"$(PROJECT_DIR)",
);
INFOPLIST_FILE = Runner/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -649,7 +649,7 @@
"$(PROJECT_DIR)",
);
INFOPLIST_FILE = Runner/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -690,7 +690,7 @@
"$(PROJECT_DIR)",
);
INFOPLIST_FILE = Runner/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
diff --git a/lib/core/background_sync.dart b/lib/core/background_sync.dart
index 96df73d19..9eed19408 100644
--- a/lib/core/background_sync.dart
+++ b/lib/core/background_sync.dart
@@ -1,36 +1,111 @@
import 'dart:async';
-import 'dart:math';
+import 'dart:io';
import 'package:cake_wallet/core/key_service.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cake_wallet/di.dart';
+import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/store/settings_store.dart';
+import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
import 'package:cw_core/sync_status.dart';
+import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_type.dart';
-import 'package:flutter/foundation.dart';
-import 'package:http/http.dart' as http;
+import 'package:flutter_local_notifications/flutter_local_notifications.dart';
+import 'package:shared_preferences/shared_preferences.dart';
class BackgroundSync {
+ final FlutterLocalNotificationsPlugin _notificationsPlugin = FlutterLocalNotificationsPlugin();
+ bool _isInitialized = false;
+
+ Future _initializeNotifications() async {
+ if (_isInitialized) return;
+
+ const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
+
+ const iosSettings = DarwinInitializationSettings(
+ requestAlertPermission: true,
+ requestBadgePermission: true,
+ requestSoundPermission: true,
+ );
+
+ const initializationSettings = InitializationSettings(
+ android: androidSettings,
+ iOS: iosSettings,
+ );
+
+ await _notificationsPlugin.initialize(initializationSettings);
+ _isInitialized = true;
+ }
+
+ Future requestPermissions() async {
+ if (Platform.isIOS || Platform.isMacOS) {
+ return await _notificationsPlugin
+ .resolvePlatformSpecificImplementation()
+ ?.requestPermissions(
+ alert: true,
+ badge: true,
+ sound: true,
+ ) ?? false;
+ } else if (Platform.isAndroid) {
+ return await _notificationsPlugin
+ .resolvePlatformSpecificImplementation()
+ ?.areNotificationsEnabled() ?? false;
+ }
+ return false;
+ }
+
+ Future showNotification(String title, String content) async {
+ await _initializeNotifications();
+ final hasPermission = await requestPermissions();
+
+ if (!hasPermission) {
+ printV('Notification permissions not granted');
+ return;
+ }
+
+ const androidDetails = AndroidNotificationDetails(
+ 'transactions',
+ 'Transactions',
+ channelDescription: 'Channel for notifications about transactions',
+ importance: Importance.defaultImportance,
+ priority: Priority.defaultPriority,
+ );
+
+ const iosDetails = DarwinNotificationDetails();
+
+ const notificationDetails = NotificationDetails(
+ android: androidDetails,
+ iOS: iosDetails,
+ );
+
+ await _notificationsPlugin.show(
+ DateTime.now().millisecondsSinceEpoch.hashCode,
+ title,
+ content,
+ notificationDetails,
+ );
+ }
+
Future sync() async {
printV("Background sync started");
- await _syncMonero();
+ await _syncWallets();
printV("Background sync completed");
}
- Future _syncMonero() async {
+ Future _syncWallets() async {
final walletLoadingService = getIt.get();
final walletListViewModel = getIt.get();
final settingsStore = getIt.get();
final List moneroWallets = walletListViewModel.wallets
.where((element) => !element.isHardware)
- .where((element) => [WalletType.monero].contains(element.type))
+ .where((element) => ![WalletType.haven, WalletType.decred].contains(element.type))
.toList();
for (int i = 0; i < moneroWallets.length; i++) {
- final wallet = await walletLoadingService.load(moneroWallets[i].type, moneroWallets[i].name);
+ final wallet = await walletLoadingService.load(moneroWallets[i].type, moneroWallets[i].name, isBackground: true);
int syncedTicks = 0;
final keyService = getIt.get();
@@ -77,7 +152,7 @@ class BackgroundSync {
} else {
syncedTicks = 0;
}
- if (kDebugMode) {
+ if (FeatureFlag.hasDevOptions) {
if (syncStatus is SyncingSyncStatus) {
final blocksLeft = syncStatus.blocksLeft;
printV("$blocksLeft Blocks Left");
@@ -102,6 +177,27 @@ class BackgroundSync {
}
}
}
+ final txs = wallet.transactionHistory;
+ final sortedTxs = txs.transactions.values.toList()..sort((a, b) => a.date.compareTo(b.date));
+ final sharedPreferences = await SharedPreferences.getInstance();
+ for (final tx in sortedTxs) {
+ final lastTriggerString = sharedPreferences.getString(PreferencesKey.backgroundSyncLastTrigger(wallet.name));
+ final lastTriggerDate = lastTriggerString != null
+ ? DateTime.parse(lastTriggerString)
+ : DateTime.now();
+ final keys = sharedPreferences.getKeys();
+ if (tx.date.isBefore(lastTriggerDate)) {
+ printV("w: ${wallet.name}, tx: ${tx.date} is before $lastTriggerDate (lastTriggerString: $lastTriggerString) (k: ${keys.length})");
+ continue;
+ }
+ await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name), tx.date.add(Duration(minutes: 1)).toIso8601String());
+ final action = tx.direction == TransactionDirection.incoming ? "Received" : "Sent";
+ if (sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false) {
+ await showNotification("$action ${wallet.currency.fullName} in ${wallet.name}", "${tx.amountFormatted()}");
+ }
+ printV("${wallet.currency.fullName} in ${wallet.name}: TX: ${tx.date} ${tx.amount} ${tx.direction}");
+ }
+ wallet.id;
await wallet.stopBackgroundSync(await keyService.getWalletPassword(walletName: wallet.name));
await wallet.close(shouldCleanup: true);
}
diff --git a/lib/core/wallet_connect/chain_service/chain_service.dart b/lib/core/wallet_connect/chain_service/chain_service.dart
deleted file mode 100644
index 1e3ce3efd..000000000
--- a/lib/core/wallet_connect/chain_service/chain_service.dart
+++ /dev/null
@@ -1,5 +0,0 @@
-abstract class ChainService {
- String getNamespace();
- String getChainId();
- List getEvents();
-}
diff --git a/lib/core/wallet_connect/chain_service/eth/evm_chain_service.dart b/lib/core/wallet_connect/chain_service/eth/evm_chain_service.dart
deleted file mode 100644
index 6f3c8fa98..000000000
--- a/lib/core/wallet_connect/chain_service/eth/evm_chain_service.dart
+++ /dev/null
@@ -1,304 +0,0 @@
-import 'dart:convert';
-import 'dart:developer';
-import 'dart:typed_data';
-
-import 'package:cake_wallet/core/wallet_connect/eth_transaction_model.dart';
-import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart';
-import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/reactions/wallet_connect.dart';
-import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart';
-import 'package:cake_wallet/store/app_store.dart';
-import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart';
-import 'package:cake_wallet/core/wallet_connect/models/connection_model.dart';
-import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_widget.dart';
-import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart';
-import 'package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart';
-import 'package:convert/convert.dart';
-import 'package:eth_sig_util/eth_sig_util.dart';
-import 'package:eth_sig_util/util/utils.dart';
-import 'package:http/http.dart' as http;
-import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
-import 'package:web3dart/web3dart.dart';
-import '../chain_service.dart';
-import '../../wallet_connect_key_service.dart';
-
-class EvmChainServiceImpl implements ChainService {
- final AppStore appStore;
- final BottomSheetService bottomSheetService;
- final Web3Wallet wallet;
- final WalletConnectKeyService wcKeyService;
-
- static const namespace = 'eip155';
- static const pSign = 'personal_sign';
- static const eSign = 'eth_sign';
- static const eSignTransaction = 'eth_signTransaction';
- static const eSignTypedData = 'eth_signTypedData_v4';
- static const eSendTransaction = 'eth_sendTransaction';
-
- final EVMChainId reference;
-
- final Web3Client ethClient;
-
- EvmChainServiceImpl({
- required this.reference,
- required this.appStore,
- required this.wcKeyService,
- required this.bottomSheetService,
- required this.wallet,
- Web3Client? web3Client,
- }) : ethClient = web3Client ??
- Web3Client(
- appStore.settingsStore.getCurrentNode(appStore.wallet!.type).uri.toString(),
- http.Client(),
- ) {
- for (final String event in getEvents()) {
- wallet.registerEventEmitter(chainId: getChainId(), event: event);
- }
- wallet.registerRequestHandler(
- chainId: getChainId(),
- method: pSign,
- handler: personalSign,
- );
- wallet.registerRequestHandler(
- chainId: getChainId(),
- method: eSign,
- handler: ethSign,
- );
- wallet.registerRequestHandler(
- chainId: getChainId(),
- method: eSignTransaction,
- handler: ethSignTransaction,
- );
- wallet.registerRequestHandler(
- chainId: getChainId(),
- method: eSendTransaction,
- handler: ethSignTransaction,
- );
- wallet.registerRequestHandler(
- chainId: getChainId(),
- method: eSignTypedData,
- handler: ethSignTypedData,
- );
- }
-
- @override
- String getNamespace() {
- return namespace;
- }
-
- @override
- String getChainId() {
- return reference.chain();
- }
-
- @override
- List getEvents() {
- return ['chainChanged', 'accountsChanged'];
- }
-
- Future requestAuthorization(String? text) async {
- // Show the bottom sheet
- final bool? isApproved = await bottomSheetService.queueBottomSheet(
- widget: Web3RequestModal(
- child: ConnectionWidget(
- title: S.current.signTransaction,
- info: [
- ConnectionModel(
- text: text,
- ),
- ],
- ),
- ),
- ) as bool?;
-
- if (isApproved != null && isApproved == false) {
- return 'User rejected signature';
- }
-
- return null;
- }
-
- Future personalSign(String topic, dynamic parameters) async {
- log('received personal sign request: $parameters');
-
- final String message;
- if (parameters[0] == null) {
- message = '';
- } else {
- message = parameters[0].toString().utf8Message;
- }
-
- final String? authError = await requestAuthorization(message);
-
- if (authError != null) {
- return authError;
- }
-
- try {
- // Load the private key
- final List keys = wcKeyService
- .getKeysForChain(appStore.wallet!);
-
- final Credentials credentials = EthPrivateKey.fromHex(keys[0].privateKey);
-
- final String signature = hex.encode(
- credentials.signPersonalMessageToUint8List(Uint8List.fromList(utf8.encode(message))),
- );
-
- return '0x$signature';
- } catch (e) {
- log(e.toString());
- bottomSheetService.queueBottomSheet(
- isModalDismissible: true,
- widget: BottomSheetMessageDisplayWidget(
- message: '${S.current.errorGettingCredentials} ${e.toString()}',
- ),
- );
- return 'Failed: Error while getting credentials';
- }
- }
-
- Future ethSign(String topic, dynamic parameters) async {
- log('received eth sign request: $parameters');
-
- final String message;
- if (parameters[1] == null) {
- message = '';
- } else {
- message = parameters[1].toString().utf8Message;
- }
-
- final String? authError = await requestAuthorization(message);
- if (authError != null) {
- return authError;
- }
-
- try {
- // Load the private key
- final List keys = wcKeyService
- .getKeysForChain(appStore.wallet!);
-
- final EthPrivateKey credentials = EthPrivateKey.fromHex(keys[0].privateKey);
-
- final String signature = hex.encode(
- credentials.signPersonalMessageToUint8List(
- Uint8List.fromList(utf8.encode(message)),
- chainId: getChainIdBasedOnWalletType(appStore.wallet!.type),
- ),
- );
- log(signature);
-
- return '0x$signature';
- } catch (e) {
- log('error: ${e.toString()}');
- bottomSheetService.queueBottomSheet(
- isModalDismissible: true,
- widget: BottomSheetMessageDisplayWidget(message: '${S.current.error}: ${e.toString()}'),
- );
- return 'Failed';
- }
- }
-
- Future ethSignTransaction(String topic, dynamic parameters) async {
- log('received eth sign transaction request: $parameters');
-
- final paramsData = parameters[0] as Map;
-
- final message = _convertToReadable(paramsData);
-
- final String? authError = await requestAuthorization(message);
-
- if (authError != null) {
- return authError;
- }
-
- // Load the private key
- final List keys = wcKeyService
- .getKeysForChain(appStore.wallet!);
-
- final Credentials credentials = EthPrivateKey.fromHex(keys[0].privateKey);
-
- WCEthereumTransactionModel ethTransaction =
- WCEthereumTransactionModel.fromJson(parameters[0] as Map);
-
- final transaction = Transaction(
- from: EthereumAddress.fromHex(ethTransaction.from),
- to: EthereumAddress.fromHex(ethTransaction.to),
- maxGas: ethTransaction.gasLimit != null ? int.tryParse(ethTransaction.gasLimit ?? "") : null,
- gasPrice: ethTransaction.gasPrice != null
- ? EtherAmount.inWei(BigInt.parse(ethTransaction.gasPrice ?? ""))
- : null,
- value: EtherAmount.inWei(BigInt.parse(ethTransaction.value)),
- data: hexToBytes(ethTransaction.data ?? ""),
- nonce: ethTransaction.nonce != null ? int.tryParse(ethTransaction.nonce ?? "") : null,
- );
-
- try {
- final result = await ethClient.sendTransaction(
- credentials,
- transaction,
- chainId: getChainIdBasedOnWalletType(appStore.wallet!.type),
- );
-
- log('Result: $result');
-
- bottomSheetService.queueBottomSheet(
- isModalDismissible: true,
- widget: BottomSheetMessageDisplayWidget(
- message: S.current.awaitDAppProcessing,
- isError: false,
- ),
- );
-
- return result;
- } catch (e) {
- log('An error has occurred while signing transaction: ${e.toString()}');
- bottomSheetService.queueBottomSheet(
- isModalDismissible: true,
- widget: BottomSheetMessageDisplayWidget(
- message: '${S.current.errorSigningTransaction}: ${e.toString()}',
- ),
- );
- return 'Failed';
- }
- }
-
- Future ethSignTypedData(String topic, dynamic parameters) async {
- log('received eth sign typed data request: $parameters');
- final String? data = parameters[1] as String?;
-
- final String? authError = await requestAuthorization(data);
-
- if (authError != null) {
- return authError;
- }
-
- final List keys = wcKeyService
- .getKeysForChain(appStore.wallet!);
-
- return EthSigUtil.signTypedData(
- privateKey: keys[0].privateKey,
- jsonData: data ?? '',
- version: TypedDataVersion.V4,
- );
- }
-
- String _convertToReadable(Map data) {
- final tokenName = getTokenNameBasedOnWalletType(appStore.wallet!.type);
- String gas = int.parse((data['gas'] as String).substring(2), radix: 16).toString();
- String value = data['value'] != null
- ? (int.parse((data['value'] as String).substring(2), radix: 16) / 1e18).toString() +
- ' $tokenName'
- : '0 $tokenName';
- String from = data['from'] as String;
- String to = data['to'] as String;
-
- return '''
- Gas: $gas\n
- Value: $value\n
- From: $from\n
- To: $to
- ''';
- }
-}
diff --git a/lib/core/wallet_connect/chain_service/solana/entities/solana_sign_message.dart b/lib/core/wallet_connect/chain_service/solana/entities/solana_sign_message.dart
deleted file mode 100644
index e462adbb5..000000000
--- a/lib/core/wallet_connect/chain_service/solana/entities/solana_sign_message.dart
+++ /dev/null
@@ -1,28 +0,0 @@
-class SolanaSignMessage {
- final String pubkey;
- final String message;
-
- SolanaSignMessage({
- required this.pubkey,
- required this.message,
- });
-
- factory SolanaSignMessage.fromJson(Map json) {
- return SolanaSignMessage(
- pubkey: json['pubkey'] as String,
- message: json['message'] as String,
- );
- }
-
- Map toJson() {
- return {
- 'pubkey': pubkey,
- 'message': message,
- };
- }
-
- @override
- String toString() {
- return 'SolanaSignMessage(pubkey: $pubkey, message: $message)';
- }
-}
diff --git a/lib/core/wallet_connect/chain_service/solana/entities/solana_sign_transaction.dart b/lib/core/wallet_connect/chain_service/solana/entities/solana_sign_transaction.dart
deleted file mode 100644
index 2cdf4697e..000000000
--- a/lib/core/wallet_connect/chain_service/solana/entities/solana_sign_transaction.dart
+++ /dev/null
@@ -1,106 +0,0 @@
-class SolanaSignTransaction {
- final String? feePayer;
- final String? recentBlockhash;
- final String transaction;
- final List? instructions;
-
- SolanaSignTransaction({
- required this.feePayer,
- required this.recentBlockhash,
- required this.instructions,
- required this.transaction,
- });
-
- factory SolanaSignTransaction.fromJson(Map json) {
- return SolanaSignTransaction(
- feePayer:json['feePayer'] !=null ? json['feePayer'] as String: null,
- recentBlockhash: json['recentBlockhash']!=null? json['recentBlockhash'] as String: null,
- instructions:json['instructions']!=null? (json['instructions'] as List)
- .map((e) => SolanaInstruction.fromJson(e as Map))
- .toList(): null,
- transaction: json['transaction'] as String,
- );
- }
-
- Map toJson() {
- return {
- 'feePayer': feePayer,
- 'recentBlockhash': recentBlockhash,
- 'instructions': instructions,
- 'transaction': transaction,
- };
- }
-
- @override
- String toString() {
- return 'SolanaSignTransaction(feePayer: $feePayer, recentBlockhash: $recentBlockhash, instructions: $instructions, transaction: $transaction)';
- }
-}
-
-class SolanaInstruction {
- final String programId;
- final List keys;
- final List data;
-
- SolanaInstruction({
- required this.programId,
- required this.keys,
- required this.data,
- });
-
- factory SolanaInstruction.fromJson(Map json) {
- return SolanaInstruction(
- programId: json['programId'] as String,
- keys: (json['keys'] as List)
- .map((e) => SolanaKeyMetadata.fromJson(e as Map))
- .toList(),
- data: (json['data'] as List).map((e) => e as int).toList(),
- );
- }
-
- Map toJson() {
- return {
- 'programId': programId,
- 'keys': keys,
- 'data': data,
- };
- }
-
- @override
- String toString() {
- return 'SolanaInstruction(programId: $programId, keys: $keys, data: $data)';
- }
-}
-
-class SolanaKeyMetadata {
- final String pubkey;
- final bool isSigner;
- final bool isWritable;
-
- SolanaKeyMetadata({
- required this.pubkey,
- required this.isSigner,
- required this.isWritable,
- });
-
- factory SolanaKeyMetadata.fromJson(Map json) {
- return SolanaKeyMetadata(
- pubkey: json['pubkey'] as String,
- isSigner: json['isSigner'] as bool,
- isWritable: json['isWritable'] as bool,
- );
- }
-
- Map toJson() {
- return {
- 'pubkey': pubkey,
- 'isSigner': isSigner,
- 'isWritable': isWritable,
- };
- }
-
- @override
- String toString() {
- return 'SolanaKeyMetadata(pubkey: $pubkey, isSigner: $isSigner, isWritable: $isWritable)';
- }
-}
diff --git a/lib/core/wallet_connect/chain_service/solana/solana_chain_id.dart b/lib/core/wallet_connect/chain_service/solana/solana_chain_id.dart
deleted file mode 100644
index ed80a4f3f..000000000
--- a/lib/core/wallet_connect/chain_service/solana/solana_chain_id.dart
+++ /dev/null
@@ -1,30 +0,0 @@
-import 'solana_chain_service.dart';
-
-enum SolanaChainId {
- mainnet,
- // testnet,
- // devnet,
-}
-
-extension SolanaChainIdX on SolanaChainId {
- String chain() {
- String name = '';
-
- switch (this) {
- case SolanaChainId.mainnet:
- name = '4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ';
- // solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp
- break;
- // case SolanaChainId.devnet:
- // name = '8E9rvCKLFQia2Y35HXjjpWzj8weVo44K';
- // // solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1
- // break;
- // case SolanaChainId.testnet:
- // name = '';
- // // solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z
- // break;
- }
-
- return '${SolanaChainServiceImpl.namespace}:$name';
- }
-}
diff --git a/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart b/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart
deleted file mode 100644
index 722e99ee3..000000000
--- a/lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart
+++ /dev/null
@@ -1,171 +0,0 @@
-import 'dart:convert';
-import 'dart:developer';
-
-import 'package:blockchain_utils/blockchain_utils.dart';
-import 'package:cake_wallet/core/wallet_connect/chain_service/solana/entities/solana_sign_message.dart';
-import 'package:cake_wallet/core/wallet_connect/chain_service/solana/solana_chain_id.dart';
-import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart';
-import 'package:cake_wallet/core/wallet_connect/models/connection_model.dart';
-import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_widget.dart';
-import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart';
-import 'package:cw_core/solana_rpc_http_service.dart';
-import 'package:cw_core/utils/print_verbose.dart';
-import 'package:on_chain/solana/solana.dart';
-import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
-import '../chain_service.dart';
-import '../../wallet_connect_key_service.dart';
-import 'entities/solana_sign_transaction.dart';
-
-class SolanaChainServiceImpl implements ChainService {
- final BottomSheetService bottomSheetService;
- final Web3Wallet wallet;
- final WalletConnectKeyService wcKeyService;
-
- static const namespace = 'solana';
- static const solSignTransaction = 'solana_signTransaction';
- static const solSignMessage = 'solana_signMessage';
-
- final SolanaChainId reference;
-
- final SolanaProvider solanaProvider;
-
- final SolanaPrivateKey? ownerPrivateKey;
-
- SolanaChainServiceImpl({
- required this.reference,
- required this.wcKeyService,
- required this.bottomSheetService,
- required this.wallet,
- required this.ownerPrivateKey,
- required String formattedRPCUrl,
- SolanaProvider? solanaProvider,
- }) : solanaProvider =
- solanaProvider ?? SolanaProvider(SolanaRPCHTTPService(url: formattedRPCUrl)) {
- for (final String event in getEvents()) {
- wallet.registerEventEmitter(chainId: getChainId(), event: event);
- }
- wallet.registerRequestHandler(
- chainId: getChainId(),
- method: solSignTransaction,
- handler: solanaSignTransaction,
- );
- wallet.registerRequestHandler(
- chainId: getChainId(),
- method: solSignMessage,
- handler: solanaSignMessage,
- );
- }
-
- @override
- String getNamespace() {
- return namespace;
- }
-
- @override
- String getChainId() {
- return reference.chain();
- }
-
- @override
- List getEvents() {
- return ['chainChanged', 'accountsChanged'];
- }
-
- Future requestAuthorization(String? text) async {
- // Show the bottom sheet
- final bool? isApproved = await bottomSheetService.queueBottomSheet(
- widget: Web3RequestModal(
- child: ConnectionWidget(
- title: S.current.signTransaction,
- info: [
- ConnectionModel(
- text: text,
- ),
- ],
- ),
- ),
- ) as bool?;
-
- if (isApproved != null && isApproved == false) {
- return 'User rejected signature';
- }
-
- return null;
- }
-
- Future solanaSignTransaction(String topic, dynamic parameters) async {
- log('received solana sign transaction request $parameters');
-
- final solanaSignTx = SolanaSignTransaction.fromJson(parameters as Map);
-
- final String? authError = await requestAuthorization('Confirm request to sign transaction?');
-
- if (authError != null) {
- return authError;
- }
-
- try {
- // Convert transaction string to bytes
- List transactionBytes = base64Decode(solanaSignTx.transaction);
-
- final message = SolanaTransactionUtils.deserializeMessageLegacy(transactionBytes);
-
- final sign = ownerPrivateKey!.sign(message.serialize());
-
- final signature = solanaProvider.request(
- SolanaRequestSendTransaction(
- encodedTransaction: Base58Encoder.encode(sign),
- commitment: Commitment.confirmed,
- ),
- );
-
- bottomSheetService.queueBottomSheet(
- isModalDismissible: true,
- widget: BottomSheetMessageDisplayWidget(
- message: S.current.awaitDAppProcessing,
- isError: false,
- ),
- );
-
- return signature;
- } catch (e) {
- log('An error has occurred while signing transaction: ${e.toString()}');
- bottomSheetService.queueBottomSheet(
- isModalDismissible: true,
- widget: BottomSheetMessageDisplayWidget(
- message: '${S.current.errorSigningTransaction}: ${e.toString()}',
- ),
- );
- return 'Failed';
- }
- }
-
- Future solanaSignMessage(String topic, dynamic parameters) async {
- log('received solana sign message request: $parameters');
-
- final solanaSignMessage = SolanaSignMessage.fromJson(parameters as Map);
-
- final String? authError = await requestAuthorization('Confirm request to sign message?');
-
- if (authError != null) {
- return authError;
- }
- List? sign;
-
- try {
- sign = ownerPrivateKey!.sign(Base58Decoder.decode(solanaSignMessage.message));
- } catch (e) {
- printV(e);
- }
-
- if (sign == null) {
- return '';
- }
-
- final signature = Base58Encoder.encode(sign);
-
- return signature;
- }
-}
diff --git a/lib/core/wallet_connect/eth_transaction_model.dart b/lib/core/wallet_connect/eth_transaction_model.dart
deleted file mode 100644
index deb33586f..000000000
--- a/lib/core/wallet_connect/eth_transaction_model.dart
+++ /dev/null
@@ -1,60 +0,0 @@
-class WCEthereumTransactionModel {
- final String from;
- final String to;
- final String value;
- final String? nonce;
- final String? gasPrice;
- final String? maxFeePerGas;
- final String? maxPriorityFeePerGas;
- final String? gas;
- final String? gasLimit;
- final String? data;
-
- WCEthereumTransactionModel({
- required this.from,
- required this.to,
- required this.value,
- this.nonce,
- this.gasPrice,
- this.maxFeePerGas,
- this.maxPriorityFeePerGas,
- this.gas,
- this.gasLimit,
- this.data,
- });
-
- factory WCEthereumTransactionModel.fromJson(Map json) {
- return WCEthereumTransactionModel(
- from: json['from'] as String,
- to: json['to'] as String,
- value: json['value'] as String,
- nonce: json['nonce'] as String?,
- gasPrice: json['gasPrice'] as String?,
- maxFeePerGas: json['maxFeePerGas'] as String?,
- maxPriorityFeePerGas: json['maxPriorityFeePerGas'] as String?,
- gas: json['gas'] as String?,
- gasLimit: json['gasLimit'] as String?,
- data: json['data'] as String?,
- );
- }
-
- Map toJson() {
- return {
- 'from': from,
- 'to': to,
- 'value': value,
- 'nonce': nonce,
- 'gasPrice': gasPrice,
- 'maxFeePerGas': maxFeePerGas,
- 'maxPriorityFeePerGas': maxPriorityFeePerGas,
- 'gas': gas,
- 'gasLimit': gasLimit,
- 'data': data,
- };
- }
-
- @override
- String toString() {
- return 'EthereumTransactionModel(from: $from, to: $to, nonce: $nonce, gasPrice: $gasPrice, maxFeePerGas: $maxFeePerGas, maxPriorityFeePerGas: $maxPriorityFeePerGas, gas: $gas, gasLimit: $gasLimit, value: $value, data: $data)';
- }
-}
diff --git a/lib/core/wallet_connect/models/auth_request_model.dart b/lib/core/wallet_connect/models/auth_request_model.dart
deleted file mode 100644
index f7fd984c8..000000000
--- a/lib/core/wallet_connect/models/auth_request_model.dart
+++ /dev/null
@@ -1,16 +0,0 @@
-import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
-
-class AuthRequestModel {
- final String iss;
- final AuthRequest request;
-
- AuthRequestModel({
- required this.iss,
- required this.request,
- });
-
- @override
- String toString() {
- return 'AuthRequestModel(iss: $iss, request: $request)';
- }
-}
diff --git a/lib/core/wallet_connect/models/chain_key_model.dart b/lib/core/wallet_connect/models/chain_key_model.dart
deleted file mode 100644
index 5cd2764da..000000000
--- a/lib/core/wallet_connect/models/chain_key_model.dart
+++ /dev/null
@@ -1,16 +0,0 @@
-class ChainKeyModel {
- final List chains;
- final String privateKey;
- final String publicKey;
-
- ChainKeyModel({
- required this.chains,
- required this.privateKey,
- required this.publicKey,
- });
-
- @override
- String toString() {
- return 'ChainKeyModel(chains: $chains, privateKey: $privateKey, publicKey: $publicKey)';
- }
-}
diff --git a/lib/core/wallet_connect/models/session_request_model.dart b/lib/core/wallet_connect/models/session_request_model.dart
deleted file mode 100644
index 0c7a5d876..000000000
--- a/lib/core/wallet_connect/models/session_request_model.dart
+++ /dev/null
@@ -1,14 +0,0 @@
-import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
-
-class SessionRequestModel {
- final ProposalData request;
-
- SessionRequestModel({
- required this.request,
- });
-
- @override
- String toString() {
- return 'SessionRequestModel(request: $request)';
- }
-}
diff --git a/lib/core/wallet_connect/web3wallet_service.dart b/lib/core/wallet_connect/web3wallet_service.dart
deleted file mode 100644
index 898433c62..000000000
--- a/lib/core/wallet_connect/web3wallet_service.dart
+++ /dev/null
@@ -1,416 +0,0 @@
-import 'dart:async';
-import 'dart:convert';
-import 'dart:developer';
-import 'dart:typed_data';
-
-import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart';
-import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_service.dart';
-import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart';
-import 'package:cake_wallet/entities/preferences_key.dart';
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/core/wallet_connect/models/auth_request_model.dart';
-import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart';
-import 'package:cake_wallet/core/wallet_connect/models/session_request_model.dart';
-import 'package:cake_wallet/reactions/wallet_connect.dart';
-import 'package:cake_wallet/solana/solana.dart';
-import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_request_widget.dart';
-import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart';
-import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart';
-import 'package:cake_wallet/store/app_store.dart';
-import 'package:cw_core/utils/print_verbose.dart';
-import 'package:cw_core/wallet_type.dart';
-import 'package:eth_sig_util/eth_sig_util.dart';
-import 'package:flutter/material.dart';
-import 'package:mobx/mobx.dart';
-import 'package:on_chain/solana/solana.dart' hide Store;
-import 'package:shared_preferences/shared_preferences.dart';
-import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
-
-import 'chain_service/solana/solana_chain_id.dart';
-import 'chain_service/solana/solana_chain_service.dart';
-import 'wc_bottom_sheet_service.dart';
-import 'package:cake_wallet/.secrets.g.dart' as secrets;
-
-part 'web3wallet_service.g.dart';
-
-class Web3WalletService = Web3WalletServiceBase with _$Web3WalletService;
-
-abstract class Web3WalletServiceBase with Store {
- final AppStore appStore;
- final SharedPreferences sharedPreferences;
- final BottomSheetService _bottomSheetHandler;
- final WalletConnectKeyService walletKeyService;
-
- late Web3Wallet _web3Wallet;
-
- @observable
- bool isInitialized;
-
- /// The list of requests from the dapp
- /// Potential types include, but aren't limited to:
- /// [SessionProposalEvent], [AuthRequest]
- @observable
- ObservableList pairings;
-
- @observable
- ObservableList sessions;
-
- @observable
- ObservableList auth;
-
- Web3WalletServiceBase(
- this._bottomSheetHandler, this.walletKeyService, this.appStore, this.sharedPreferences)
- : pairings = ObservableList(),
- sessions = ObservableList(),
- auth = ObservableList(),
- isInitialized = false;
-
- @action
- void create() {
- // Create the web3wallet client
- _web3Wallet = Web3Wallet(
- core: Core(projectId: secrets.walletConnectProjectId),
- metadata: const PairingMetadata(
- name: 'Cake Wallet',
- description: 'Cake Wallet',
- url: 'https://cakewallet.com',
- icons: ['https://cakewallet.com/assets/image/cake_logo.png'],
- ),
- );
-
- // Setup our accounts
- List chainKeys = walletKeyService.getKeys(appStore.wallet!);
- for (final chainKey in chainKeys) {
- for (final chainId in chainKey.chains) {
- _web3Wallet.registerAccount(
- chainId: chainId,
- accountAddress: chainKey.publicKey,
- );
- }
- }
-
- // Setup our listeners
- log('Created instance of web3wallet');
- _web3Wallet.core.pairing.onPairingInvalid.subscribe(_onPairingInvalid);
- _web3Wallet.core.pairing.onPairingCreate.subscribe(_onPairingCreate);
- _web3Wallet.core.pairing.onPairingDelete.subscribe(_onPairingDelete);
- _web3Wallet.core.pairing.onPairingExpire.subscribe(_onPairingDelete);
- _web3Wallet.pairings.onSync.subscribe(_onPairingsSync);
- _web3Wallet.onSessionProposal.subscribe(_onSessionProposal);
- _web3Wallet.onSessionProposalError.subscribe(_onSessionProposalError);
- _web3Wallet.onSessionConnect.subscribe(_onSessionConnect);
- _web3Wallet.onAuthRequest.subscribe(_onAuthRequest);
- }
-
- @action
- Future init() async {
- // Await the initialization of the web3wallet
- log('Intializing web3wallet');
- if (!isInitialized) {
- try {
- await _web3Wallet.init();
- log('Initialized');
- isInitialized = true;
- } catch (e) {
- log('Experimentallllll: $e');
- isInitialized = false;
- }
- }
-
- _refreshPairings();
-
- final newSessions = _web3Wallet.sessions.getAll();
- sessions.addAll(newSessions);
-
- final newAuthRequests = _web3Wallet.completeRequests.getAll();
- auth.addAll(newAuthRequests);
-
- if (isEVMCompatibleChain(appStore.wallet!.type)) {
- for (final cId in EVMChainId.values) {
- EvmChainServiceImpl(
- reference: cId,
- appStore: appStore,
- wcKeyService: walletKeyService,
- bottomSheetService: _bottomSheetHandler,
- wallet: _web3Wallet,
- );
- }
- }
-
- if (appStore.wallet!.type == WalletType.solana) {
- for (final cId in SolanaChainId.values) {
- final node = appStore.settingsStore.getCurrentNode(appStore.wallet!.type);
-
- String formattedUrl;
- String protocolUsed = node.isSSL ? "https" : "http";
-
- if (node.uriRaw == 'rpc.ankr.com') {
- String ankrApiKey = secrets.ankrApiKey;
-
- formattedUrl = '$protocolUsed://${node.uriRaw}/$ankrApiKey';
- } else if (node.uriRaw == 'solana-mainnet.core.chainstack.com') {
- String chainStackApiKey = secrets.chainStackApiKey;
-
- formattedUrl = '$protocolUsed://${node.uriRaw}/$chainStackApiKey';
- } else {
- formattedUrl = '$protocolUsed://${node.uriRaw}';
- }
-
- SolanaChainServiceImpl(
- reference: cId,
- formattedRPCUrl: formattedUrl,
- wcKeyService: walletKeyService,
- bottomSheetService: _bottomSheetHandler,
- wallet: _web3Wallet,
- ownerPrivateKey: SolanaPrivateKey.fromSeedHex(solana!.getPrivateKey(appStore.wallet!)),
- );
- }
- }
- }
-
- @action
- FutureOr onDispose() {
- log('web3wallet dispose');
- _web3Wallet.core.pairing.onPairingInvalid.unsubscribe(_onPairingInvalid);
- _web3Wallet.pairings.onSync.unsubscribe(_onPairingsSync);
- _web3Wallet.onSessionProposal.unsubscribe(_onSessionProposal);
- _web3Wallet.onSessionProposalError.unsubscribe(_onSessionProposalError);
- _web3Wallet.onSessionConnect.unsubscribe(_onSessionConnect);
- _web3Wallet.onAuthRequest.unsubscribe(_onAuthRequest);
- _web3Wallet.core.pairing.onPairingDelete.unsubscribe(_onPairingDelete);
- _web3Wallet.core.pairing.onPairingExpire.unsubscribe(_onPairingDelete);
- isInitialized = false;
- }
-
- Web3Wallet getWeb3Wallet() {
- return _web3Wallet;
- }
-
- void _onPairingsSync(StoreSyncEvent? args) {
- if (args != null) {
- _refreshPairings();
- }
- }
-
- void _onPairingDelete(PairingEvent? event) {
- _refreshPairings();
- }
-
- Future _onSessionProposalError(SessionProposalErrorEvent? args) async {
- log(args.toString());
- }
-
- void _onSessionProposal(SessionProposalEvent? args) async {
- if (args != null) {
- final chaindIdNamespace = getChainNameSpaceAndIdBasedOnWalletType(appStore.wallet!.type);
- final Widget modalWidget = Web3RequestModal(
- child: ConnectionRequestWidget(
- chaindIdNamespace: chaindIdNamespace,
- wallet: _web3Wallet,
- sessionProposal: SessionRequestModel(request: args.params),
- ),
- );
- // show the bottom sheet
- final bool? isApproved = await _bottomSheetHandler.queueBottomSheet(
- widget: modalWidget,
- ) as bool?;
-
- if (isApproved != null && isApproved) {
- _web3Wallet.approveSession(
- id: args.id,
- namespaces: args.params.generatedNamespaces!,
- );
- } else {
- _web3Wallet.rejectSession(
- id: args.id,
- reason: Errors.getSdkError(
- Errors.USER_REJECTED,
- ),
- );
- }
- }
- }
-
- @action
- void _onPairingInvalid(PairingInvalidEvent? args) {
- log('Pairing Invalid Event: $args');
- _bottomSheetHandler.queueBottomSheet(
- isModalDismissible: true,
- widget: BottomSheetMessageDisplayWidget(message: '${S.current.pairingInvalidEvent}: $args'),
- );
- }
-
- @action
- Future pairWithUri(Uri uri) async {
- try {
- log('Pairing with URI: $uri');
- await _web3Wallet.pair(uri: uri);
- } on WalletConnectError catch (e) {
- _bottomSheetHandler.queueBottomSheet(
- isModalDismissible: true,
- widget: BottomSheetMessageDisplayWidget(message: e.message),
- );
- } catch (e) {
- _bottomSheetHandler.queueBottomSheet(
- isModalDismissible: true,
- widget: BottomSheetMessageDisplayWidget(message: e.toString()),
- );
- }
- }
-
- @action
- void _refreshPairings() {
- printV('Refreshing pairings');
- pairings.clear();
-
- final allPairings = _web3Wallet.pairings.getAll();
-
- final keyForWallet = getKeyForStoringTopicsForWallet();
-
- if (keyForWallet.isEmpty) return;
-
- final currentTopicsForWallet = getPairingTopicsForWallet(keyForWallet);
-
- final filteredPairings =
- allPairings.where((pairing) => currentTopicsForWallet.contains(pairing.topic)).toList();
-
- pairings.addAll(filteredPairings);
- }
-
- void _onPairingCreate(PairingEvent? args) {
- log('Pairing Create Event: $args');
- }
-
- @action
- Future _onSessionConnect(SessionConnect? args) async {
- if (args != null) {
- log('Session Connected $args');
-
- await savePairingTopicToLocalStorage(args.session.pairingTopic);
-
- sessions.add(args.session);
-
- _refreshPairings();
- }
- }
-
- @action
- Future _onAuthRequest(AuthRequest? args) async {
- if (args != null) {
- final chaindIdNamespace = getChainNameSpaceAndIdBasedOnWalletType(appStore.wallet!.type);
- List chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
- // Create the message to be signed
- final String iss = 'did:pkh:$chaindIdNamespace:${chainKeys.first.publicKey}';
- final Widget modalWidget = Web3RequestModal(
- child: ConnectionRequestWidget(
- chaindIdNamespace: chaindIdNamespace,
- wallet: _web3Wallet,
- authRequest: AuthRequestModel(iss: iss, request: args),
- ),
- );
- final bool? isAuthenticated = await _bottomSheetHandler.queueBottomSheet(
- widget: modalWidget,
- ) as bool?;
-
- if (isAuthenticated != null && isAuthenticated) {
- final String message = _web3Wallet.formatAuthMessage(
- iss: iss,
- cacaoPayload: CacaoRequestPayload.fromPayloadParams(
- args.payloadParams,
- ),
- );
-
- final String sig = EthSigUtil.signPersonalMessage(
- message: Uint8List.fromList(message.codeUnits),
- privateKey: chainKeys.first.privateKey,
- );
-
- await _web3Wallet.respondAuthRequest(
- id: args.id,
- iss: iss,
- signature: CacaoSignature(
- t: CacaoSignature.EIP191,
- s: sig,
- ),
- );
- } else {
- await _web3Wallet.respondAuthRequest(
- id: args.id,
- iss: iss,
- error: Errors.getSdkError(
- Errors.USER_REJECTED_AUTH,
- ),
- );
- }
- }
- }
-
- @action
- Future disconnectSession(String topic) async {
- final session = sessions.firstWhere((element) => element.pairingTopic == topic);
-
- await _web3Wallet.core.pairing.disconnect(topic: topic);
- await _web3Wallet.disconnectSession(
- topic: session.topic, reason: Errors.getSdkError(Errors.USER_DISCONNECTED));
- }
-
- @action
- List getSessionsForPairingInfo(PairingInfo pairing) {
- return sessions.where((element) => element.pairingTopic == pairing.topic).toList();
- }
-
- String getKeyForStoringTopicsForWallet() {
- List chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
-
- if (chainKeys.isEmpty) {
- return '';
- }
-
- final keyForPairingTopic =
- PreferencesKey.walletConnectPairingTopicsListForWallet(chainKeys.first.publicKey);
-
- return keyForPairingTopic;
- }
-
- List getPairingTopicsForWallet(String key) {
- // Get the JSON-encoded string from shared preferences
- final jsonString = sharedPreferences.getString(key);
-
- // If the string is null, return an empty list
- if (jsonString == null) {
- return [];
- }
-
- // Decode the JSON string to a list of strings
- final List jsonList = jsonDecode(jsonString) as List;
-
- // Cast each item to a string
- return jsonList.map((item) => item as String).toList();
- }
-
- Future savePairingTopicToLocalStorage(String pairingTopic) async {
- // Get key specific to the current wallet
- final key = getKeyForStoringTopicsForWallet();
-
- if (key.isEmpty) return;
-
- // Get all pairing topics attached to this key
- final pairingTopicsForWallet = getPairingTopicsForWallet(key);
-
- printV(pairingTopicsForWallet);
-
- bool isPairingTopicAlreadySaved = pairingTopicsForWallet.contains(pairingTopic);
- printV('Is Pairing Topic Saved: $isPairingTopicAlreadySaved');
-
- if (!isPairingTopicAlreadySaved) {
- // Update the list with the most recent pairing topic
- pairingTopicsForWallet.add(pairingTopic);
-
- // Convert the list of updated pairing topics to a JSON-encoded string
- final jsonString = jsonEncode(pairingTopicsForWallet);
-
- // Save the encoded string to shared preferences
- await sharedPreferences.setString(key, jsonString);
- }
- }
-}
diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart
index c92adfa4e..437ef2df9 100644
--- a/lib/core/wallet_loading_service.dart
+++ b/lib/core/wallet_loading_service.dart
@@ -52,8 +52,11 @@ class WalletLoadingService {
}
}
- Future load(WalletType type, String name, {String? password}) async {
+ Future load(WalletType type, String name, {String? password, bool isBackground = false}) async {
try {
+ if (!isBackground) {
+ await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(name), DateTime.now().toIso8601String());
+ }
final walletService = walletServiceFactory.call(type);
final walletPassword = password ?? (await keyService.getWalletPassword(walletName: name));
final wallet = await walletService.openWallet(name, walletPassword);
diff --git a/lib/di.dart b/lib/di.dart
index b06183cde..af3d590d4 100644
--- a/lib/di.dart
+++ b/lib/di.dart
@@ -20,9 +20,6 @@ import 'package:cake_wallet/core/new_wallet_type_arguments.dart';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
-import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart';
-import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
-import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart';
import 'package:cake_wallet/core/wallet_creation_service.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cake_wallet/core/yat_service.dart';
@@ -36,8 +33,13 @@ import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
import 'package:cake_wallet/haven/cw_haven.dart';
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
+import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart';
import 'package:cake_wallet/src/screens/settings/background_sync_page.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
import 'package:cake_wallet/view_model/dev/monero_background_sync.dart';
+import 'package:cake_wallet/view_model/dev/shared_preferences.dart';
import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
@@ -267,6 +269,8 @@ import 'buy/kryptonim/kryptonim.dart';
import 'buy/meld/meld_buy_provider.dart';
import 'src/screens/buy/buy_sell_page.dart';
import 'cake_pay/cake_pay_payment_credantials.dart';
+import 'package:cake_wallet/view_model/dev/background_sync_logs_view_model.dart';
+import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart';
final getIt = GetIt.instance;
@@ -635,11 +639,15 @@ Future setup({
getIt.registerLazySingleton(() => KeyServiceImpl());
- getIt.registerLazySingleton(() {
- final Web3WalletService web3WalletService = Web3WalletService(getIt.get(),
- getIt.get(), appStore, getIt.get());
- web3WalletService.create();
- return web3WalletService;
+ getIt.registerLazySingleton(() {
+ final WalletKitService walletKitService = WalletKitService(
+ getIt.get(),
+ getIt.get(),
+ appStore,
+ getIt.get(),
+ );
+ walletKitService.create();
+ return walletKitService;
});
getIt.registerFactory(() => BalancePage(
@@ -899,9 +907,7 @@ Future setup({
nanoAccountCreationViewModel:
getIt.get(param1: account)));
- getIt.registerFactory(() {
- return DisplaySettingsViewModel(getIt.get());
- });
+ getIt.registerFactory(() => DisplaySettingsViewModel(getIt.get()));
getIt.registerFactory(
() => SilentPaymentsSettingsViewModel(
@@ -913,23 +919,18 @@ Future setup({
getIt.registerFactory(
() => MwebSettingsViewModel(getIt.get(), getIt.get().wallet!));
- getIt.registerFactory(() {
- return PrivacySettingsViewModel(getIt.get(), getIt.get().wallet!);
- });
+ getIt.registerFactory(
+ () => PrivacySettingsViewModel(getIt.get(), getIt.get().wallet!));
getIt.registerFactory(() => TrocadorExchangeProvider());
getIt.registerFactory(() => TrocadorProvidersViewModel(
getIt.get(), getIt.get()));
- getIt.registerFactory(() {
- return OtherSettingsViewModel(
- getIt.get(), getIt.get().wallet!, getIt.get());
- });
+ getIt.registerFactory(() => OtherSettingsViewModel(
+ getIt.get(), getIt.get().wallet!, getIt.get()));
- getIt.registerFactory(() {
- return SecuritySettingsViewModel(getIt.get());
- });
+ getIt.registerFactory(() => SecuritySettingsViewModel(getIt.get()));
getIt.registerFactory(() => WalletSeedViewModel(getIt.get().wallet!));
@@ -938,6 +939,8 @@ Future setup({
getIt.registerFactory(() => DevMoneroBackgroundSync(getIt.get().wallet!));
+ getIt.registerFactory(() => DevSharedPreferences());
+
getIt.registerFactoryParam((bool isWalletCreated, _) =>
WalletSeedPage(getIt.get(), isNewWalletCreated: isWalletCreated));
@@ -1472,7 +1475,8 @@ Future setup({
});
getIt.registerFactory(
- () => WalletConnectConnectionsView(web3walletService: getIt.get()));
+ () => WalletConnectConnectionsView(walletKitService: getIt.get()),
+ );
getIt.registerFactory(() => NFTViewModel(appStore, getIt.get()));
getIt.registerFactory(() => TorPage(getIt.get()));
@@ -1482,6 +1486,14 @@ Future setup({
getIt.registerFactory(() => SeedVerificationPage(getIt.get()));
getIt.registerFactory(() => DevMoneroBackgroundSyncPage(getIt.get()));
+
getIt.registerFactory(() => DevMoneroCallProfilerPage());
+
+ getIt.registerFactory(() => DevSharedPreferencesPage(getIt.get()));
+
+ getIt.registerFactory(() => BackgroundSyncLogsViewModel());
+
+ getIt.registerFactory(() => DevBackgroundSyncLogsPage(getIt.get()));
+
_isSetupFinished = true;
}
diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart
index cd189086e..79de3cbeb 100644
--- a/lib/entities/preferences_key.dart
+++ b/lib/entities/preferences_key.dart
@@ -104,4 +104,6 @@ class PreferencesKey {
static const walletConnectPairingTopicsList = 'wallet_connect_pairing_topics_list';
static String walletConnectPairingTopicsListForWallet(String publicKey) =>
'${PreferencesKey.walletConnectPairingTopicsList}_${publicKey}';
+ static String backgroundSyncLastTrigger(String walletId) => 'background_sync_last_trigger_${walletId}';
+ static const backgroundSyncNotificationsEnabled = 'background_sync_notifications_enabled';
}
diff --git a/lib/reactions/on_authentication_state_change.dart b/lib/reactions/on_authentication_state_change.dart
index bce160b4a..46ac481bb 100644
--- a/lib/reactions/on_authentication_state_change.dart
+++ b/lib/reactions/on_authentication_state_change.dart
@@ -1,6 +1,5 @@
import 'dart:async';
-import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/hardware_wallet/require_hardware_wallet_connection.dart';
import 'package:cake_wallet/entities/load_current_wallet.dart';
@@ -8,6 +7,7 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/store/authentication_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
@@ -71,7 +71,7 @@ void startAuthenticationStateChange(
buttonAction: () => Navigator.of(context).pop()),
);
await loadCurrentWallet();
- getIt.get().resetCurrentSheet();
+ getIt.get().showNext();
await navigatorKey.currentState!
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
},
diff --git a/lib/reactions/wallet_connect.dart b/lib/reactions/wallet_connect.dart
index 217664eaa..37a0b92ec 100644
--- a/lib/reactions/wallet_connect.dart
+++ b/lib/reactions/wallet_connect.dart
@@ -1,5 +1,7 @@
-import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart';
-import 'package:cake_wallet/core/wallet_connect/chain_service/solana/solana_chain_id.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/eth/evm_chain_id.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/eth/evm_supported_methods.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/solana/solana_chain_id.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/solana/solana_supported_methods.dart';
import 'package:cw_core/wallet_type.dart';
bool isEVMCompatibleChain(WalletType walletType) {
@@ -47,6 +49,19 @@ String getChainNameSpaceAndIdBasedOnWalletType(WalletType walletType) {
}
}
+List getChainSupportedMethodsOnWalletType(WalletType walletType) {
+ switch (walletType) {
+ case WalletType.ethereum:
+ return EVMSupportedMethods.values.map((e) => e.name).toList();
+ case WalletType.polygon:
+ return EVMSupportedMethods.values.map((e) => e.name).toList();
+ case WalletType.solana:
+ return SolanaSupportedMethods.values.map((e) => e.name).toList();
+ default:
+ return [];
+ }
+}
+
int getChainIdBasedOnWalletType(WalletType walletType) {
switch (walletType) {
case WalletType.polygon:
diff --git a/lib/router.dart b/lib/router.dart
index de7b39b96..1073c27a3 100644
--- a/lib/router.dart
+++ b/lib/router.dart
@@ -4,7 +4,6 @@ import 'package:cake_wallet/core/new_wallet_arguments.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/core/new_wallet_type_arguments.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
-import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/qr_view_data.dart';
@@ -37,6 +36,8 @@ import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart';
import 'package:cake_wallet/src/screens/dashboard/sign_page.dart';
import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
+import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart';
+import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart';
import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart';
import 'package:cake_wallet/src/screens/exchange/exchange_page.dart';
import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart';
@@ -106,6 +107,7 @@ import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart';
import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart';
import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart';
import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart';
import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart';
@@ -795,7 +797,7 @@ Route createRoute(RouteSettings settings) {
case Routes.walletConnectConnectionsListing:
return MaterialPageRoute(
builder: (_) => WalletConnectConnectionsView(
- web3walletService: getIt.get(),
+ walletKitService: getIt.get(),
launchUri: settings.arguments as Uri?,
));
@@ -856,6 +858,15 @@ Route createRoute(RouteSettings settings) {
return MaterialPageRoute(
builder: (_) => getIt.get(),
);
+ case Routes.devSharedPreferences:
+ return MaterialPageRoute(
+ builder: (_) => getIt.get(),
+ );
+
+ case Routes.devBackgroundSyncLogs:
+ return MaterialPageRoute(
+ builder: (_) => getIt.get(),
+ );
case Routes.devMoneroCallProfiler:
return MaterialPageRoute(
diff --git a/lib/routes.dart b/lib/routes.dart
index f7f3e2f6f..a9a637c75 100644
--- a/lib/routes.dart
+++ b/lib/routes.dart
@@ -111,8 +111,12 @@ class Routes {
static const importNFTPage = '/import_nft_page';
static const torPage = '/tor_page';
static const backgroundSync = '/background_sync';
+
static const devMoneroBackgroundSync = '/dev/monero_background_sync';
static const devMoneroCallProfiler = '/dev/monero_call_profiler';
+ static const devSharedPreferences = '/dev/shared_preferences';
+ static const devBackgroundSyncLogs = '/dev/background_sync_logs';
+
static const signPage = '/sign_page';
static const connectDevices = '/device/connect';
static const urqrAnimatedPage = '/urqr/animated_page';
diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart
index cf1f6fa17..52cc82dde 100644
--- a/lib/src/screens/dashboard/dashboard_page.dart
+++ b/lib/src/screens/dashboard/dashboard_page.dart
@@ -1,10 +1,10 @@
import 'dart:async';
-import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/cake_features_page.dart';
-import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_listener_widget.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/widgets/gradient_background.dart';
import 'package:cake_wallet/src/widgets/haven_wallet_removal_popup.dart';
import 'package:cake_wallet/src/widgets/services_updates_widget.dart';
diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart
index d9b509136..7f817fe5a 100644
--- a/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart
+++ b/lib/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart
@@ -1,4 +1,3 @@
-import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
@@ -8,7 +7,8 @@ import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sideba
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart';
-import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_listener_widget.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/widgets/services_updates_widget.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart';
diff --git a/lib/src/screens/dev/background_sync_logs_page.dart b/lib/src/screens/dev/background_sync_logs_page.dart
new file mode 100644
index 000000000..b269083f2
--- /dev/null
+++ b/lib/src/screens/dev/background_sync_logs_page.dart
@@ -0,0 +1,314 @@
+import 'package:cake_wallet/src/screens/base_page.dart';
+import 'package:cake_wallet/view_model/dev/background_sync_logs_view_model.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_mobx/flutter_mobx.dart';
+import 'package:intl/intl.dart';
+
+class DevBackgroundSyncLogsPage extends BasePage {
+ final BackgroundSyncLogsViewModel viewModel;
+
+ DevBackgroundSyncLogsPage(this.viewModel) {
+ viewModel.loadLogs();
+ }
+
+ @override
+ String? get title => "[dev] background sync logs";
+
+ @override
+ Widget? trailing(BuildContext context) {
+ return IconButton(
+ icon: Icon(Icons.refresh),
+ onPressed: () => viewModel.loadLogs(),
+ );
+ }
+
+ @override
+ Widget body(BuildContext context) {
+ return Observer(
+ builder: (_) {
+ if (viewModel.isLoading) {
+ return Center(child: CircularProgressIndicator());
+ }
+
+ if (viewModel.error != null) {
+ return Center(child: Text("Error: ${viewModel.error}"));
+ }
+
+ if (viewModel.logData == null) {
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text("No logs loaded"),
+ SizedBox(height: 16),
+ ElevatedButton(
+ onPressed: () => viewModel.loadLogs(),
+ child: Text("Load Logs"),
+ ),
+ ],
+ ),
+ );
+ }
+
+ return DefaultTabController(
+ length: 2,
+ child: Column(
+ children: [
+ TabBar(
+ tabs: [
+ Tab(text: "Logs (${viewModel.logs.length})"),
+ Tab(text: "Sessions (${viewModel.sessions.length})"),
+ ],
+ ),
+ Expanded(
+ child: TabBarView(
+ children: [
+ _buildLogsTab(context),
+ _buildSessionsTab(context),
+ ],
+ ),
+ ),
+ _buildActionButtons(context),
+ ],
+ ),
+ );
+ },
+ );
+ }
+
+ Widget _buildLogsTab(BuildContext context) {
+ final logs = viewModel.logs;
+ if (logs.isEmpty) {
+ return Center(child: Text("No logs available"));
+ }
+
+ final dateFormat = DateFormat('yyyy-MM-dd HH:mm:ss.SSS');
+
+ return ListView.builder(
+ itemCount: logs.length,
+ itemBuilder: (context, index) {
+ final log = logs[index];
+ return ListTile(
+ title: Text(
+ log.message,
+ style: TextStyle(
+ fontSize: 14,
+ fontFamily: 'Monospace',
+ ),
+ ),
+ subtitle: Text(
+ '${dateFormat.format(log.timestamp)} | ${log.level}' +
+ (log.sessionId != null ? ' | Session: ${log.sessionId}' : ''),
+ style: TextStyle(
+ fontSize: 12,
+ color: _getLevelColor(log.level),
+ ),
+ ),
+ dense: true,
+ onTap: () {
+ Clipboard.setData(ClipboardData(text: log.message));
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Log message copied to clipboard')),
+ );
+ },
+ onLongPress: () {
+ Clipboard.setData(ClipboardData(
+ text: '${dateFormat.format(log.timestamp)} [${log.level}] ${log.message}'));
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Full log entry copied to clipboard')),
+ );
+ },
+ tileColor: index % 2 == 0 ? Colors.transparent : Colors.black.withOpacity(0.03),
+ );
+ },
+ );
+ }
+
+ Widget _buildSessionsTab(BuildContext context) {
+ final sessions = viewModel.sessions;
+ if (sessions.isEmpty) {
+ return Center(child: Text("No sessions available"));
+ }
+
+ final dateFormat = DateFormat('yyyy-MM-dd HH:mm:ss');
+
+ return ListView.builder(
+ itemCount: sessions.length,
+ itemBuilder: (context, index) {
+ final session = sessions[index];
+ final isActive = session.endTime == null;
+
+ return ExpansionTile(
+ title: Text(
+ session.name,
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ color: isActive ? Colors.green : null,
+ ),
+ ),
+ subtitle: Text(
+ 'ID: ${session.id} | Started: ${dateFormat.format(session.startTime)}',
+ style: TextStyle(fontSize: 12),
+ ),
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Start: ${session.startTime.toString()}'),
+ if (session.endTime != null)
+ Text('End: ${session.endTime.toString()}'),
+ if (session.duration != null)
+ Text('Duration: ${_formatDuration(session.duration!)}'),
+ SizedBox(height: 8),
+ _buildSessionLogs(context, session.id),
+ ],
+ ),
+ ),
+ ],
+ );
+ },
+ );
+ }
+
+ Widget _buildSessionLogs(BuildContext context, int sessionId) {
+ final sessionLogs = viewModel.logs
+ .where((log) => log.sessionId == sessionId)
+ .toList();
+
+ if (sessionLogs.isEmpty) {
+ return Text('No logs for this session');
+ }
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Session Logs (${sessionLogs.length}):',
+ style: TextStyle(fontWeight: FontWeight.bold)),
+ SizedBox(height: 8),
+ Container(
+ height: 200,
+ decoration: BoxDecoration(
+ color: Colors.black.withOpacity(0.05),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: ListView.builder(
+ itemCount: sessionLogs.length,
+ itemBuilder: (context, index) {
+ final log = sessionLogs[index];
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
+ child: Text(
+ '[${log.level}] ${log.message}',
+ style: TextStyle(
+ fontSize: 12,
+ fontFamily: 'Monospace',
+ color: _getLevelColor(log.level),
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildActionButtons(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ ElevatedButton.icon(
+ icon: Icon(Icons.refresh),
+ label: Text('Refresh'),
+ onPressed: () => viewModel.loadLogs(),
+ ),
+ ElevatedButton.icon(
+ icon: Icon(Icons.copy),
+ label: Text('Copy All'),
+ onPressed: () => _copyAllLogs(context),
+ ),
+ ElevatedButton.icon(
+ icon: Icon(Icons.delete),
+ label: Text('Clear'),
+ onPressed: () => _confirmClearLogs(context),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.red,
+ foregroundColor: Colors.white,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ void _copyAllLogs(BuildContext context) {
+ if (viewModel.logData == null) return;
+
+ final buffer = StringBuffer();
+ final dateFormat = DateFormat('yyyy-MM-dd HH:mm:ss.SSS');
+
+ for (final log in viewModel.logs) {
+ buffer.writeln('${dateFormat.format(log.timestamp)} [${log.level}] ${log.message}');
+ }
+
+ Clipboard.setData(ClipboardData(text: buffer.toString()));
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('All logs copied to clipboard')),
+ );
+ }
+
+ void _confirmClearLogs(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: Text('Clear Logs'),
+ content: Text('Are you sure you want to clear the logs display?'),
+ actions: [
+ TextButton(
+ child: Text('Cancel'),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ TextButton(
+ child: Text('Clear'),
+ style: TextButton.styleFrom(foregroundColor: Colors.red),
+ onPressed: () {
+ viewModel.clearLogs();
+ Navigator.of(context).pop();
+ },
+ ),
+ ],
+ );
+ },
+ );
+ }
+
+ Color _getLevelColor(String level) {
+ switch (level.toLowerCase()) {
+ case 'error':
+ return Colors.red;
+ case 'warning':
+ return Colors.orange;
+ case 'info':
+ return Colors.blue;
+ case 'debug':
+ return Colors.green;
+ case 'trace':
+ return Colors.purple;
+ default:
+ return Colors.grey;
+ }
+ }
+
+ String _formatDuration(Duration duration) {
+ String twoDigits(int n) => n.toString().padLeft(2, '0');
+ String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
+ String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
+ return '${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds';
+ }
+}
\ No newline at end of file
diff --git a/lib/src/screens/dev/shared_preferences_page.dart b/lib/src/screens/dev/shared_preferences_page.dart
new file mode 100644
index 000000000..cb21aaa78
--- /dev/null
+++ b/lib/src/screens/dev/shared_preferences_page.dart
@@ -0,0 +1,404 @@
+import 'package:cake_wallet/src/screens/base_page.dart';
+import 'package:cake_wallet/view_model/dev/shared_preferences.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_mobx/flutter_mobx.dart';
+
+class DevSharedPreferencesPage extends BasePage {
+ final DevSharedPreferences viewModel;
+
+ DevSharedPreferencesPage(this.viewModel);
+
+ @override
+ String? get title => "[dev] shared preferences";
+
+ @override
+ Widget? trailing(BuildContext context) {
+ return IconButton(
+ icon: Icon(Icons.add),
+ onPressed: () => _showCreateDialog(context),
+ );
+ }
+
+ @override
+ Widget body(BuildContext context) {
+ return Observer(
+ builder: (_) {
+ if (viewModel.sharedPreferences == null) {
+ return Center(child: Text("No shared preferences found"));
+ }
+ final keys = viewModel.keys;
+ Map values = {};
+ for (final key in keys) {
+ values[key] = viewModel.get(key);
+ }
+ Map types = {};
+ for (final key in keys) {
+ types[key] = viewModel.getPreferenceType(key);
+ }
+ return ListView.builder(
+ itemCount: keys.length,
+ itemBuilder: (context, index) {
+ final key = keys[index];
+ final type = types[key]!;
+ return ListTile(
+ onTap: () {
+ Clipboard.setData(ClipboardData(text: key + ": " + values[key].toString()));
+ },
+ onLongPress: () {
+ _showEditDialog(context, key, type, values[key]);
+ },
+ title: switch (type) {
+ PreferenceType.bool => Text(key, style: TextStyle(color: Colors.blue)),
+ PreferenceType.int => Text(key, style: TextStyle(color: Colors.green)),
+ PreferenceType.double => Text(key, style: TextStyle(color: Colors.yellow)),
+ PreferenceType.listString => Text(key, style: TextStyle(color: Colors.purple)),
+ PreferenceType.string => Text(key),
+ PreferenceType.unknown => Text(key),
+ },
+ subtitle: switch (type) {
+ PreferenceType.bool => Text("bool: ${values[key]}"),
+ PreferenceType.int => Text("int: ${values[key]}"),
+ PreferenceType.double => Text("double: ${values[key]}"),
+ PreferenceType.listString => values[key].isEmpty as bool ? Text("listString: []") : Text("listString:\n- ${values[key].join("\n- ")}"),
+ PreferenceType.string => Text("string: ${values[key]}"),
+ PreferenceType.unknown => Text("UNKNOWN(${values[key].runtimeType}): ${values[key]}"),
+ },
+ );
+ },
+ );
+ },
+ );
+ }
+
+ void _showEditDialog(BuildContext context, String key, PreferenceType type, dynamic currentValue) {
+ dynamic newValue = currentValue;
+ bool isListString = type == PreferenceType.listString;
+ List listItems = isListString ? List.from(currentValue as Iterable) : [];
+ TextEditingController textController = TextEditingController(
+ text: isListString ? '' : currentValue?.toString() ?? '');
+
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return StatefulBuilder(
+ builder: (context, setState) {
+ return AlertDialog(
+ title: Text('Edit $key'),
+ content: SizedBox(
+ width: double.maxFinite,
+ height: double.maxFinite,
+ child: SingleChildScrollView(
+ child: _buildDialogContent(
+ type,
+ newValue,
+ listItems,
+ textController,
+ (value) => setState(() => newValue = value),
+ (items) => setState(() => listItems = items),
+ ),
+ ),
+ ),
+ actions: [
+ TextButton(
+ child: Text('Delete'),
+ style: TextButton.styleFrom(foregroundColor: Colors.red),
+ onPressed: () {
+ _showDeleteConfirmation(context, key);
+ },
+ ),
+ TextButton(
+ child: Text('Cancel'),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ TextButton(
+ child: Text('Save'),
+ onPressed: () async {
+ if (_validateAndUpdateValue(
+ context,
+ type,
+ textController,
+ listItems,
+ (value) => newValue = value
+ )) {
+ await viewModel.set(key, type, newValue);
+ Navigator.of(context).pop();
+ }
+ },
+ ),
+ ],
+ );
+ },
+ );
+ },
+ );
+ }
+
+ void _showDeleteConfirmation(BuildContext context, String key) {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: Text('Delete Preference'),
+ content: Text('Are you sure you want to delete "$key"?'),
+ actions: [
+ TextButton(
+ child: Text('Cancel'),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ TextButton(
+ child: Text('Delete'),
+ style: TextButton.styleFrom(foregroundColor: Colors.red),
+ onPressed: () {
+ viewModel.delete(key);
+ Navigator.of(context).pop();
+ Navigator.of(context).pop();
+ },
+ ),
+ ],
+ );
+ },
+ );
+ }
+
+ Widget _buildDialogContent(
+ PreferenceType type,
+ dynamic value,
+ List listItems,
+ TextEditingController textController,
+ Function(dynamic) onValueChanged,
+ Function(List) onListChanged,
+ ) {
+ return switch (type) {
+ PreferenceType.bool => _buildBoolEditor(value as bool, onValueChanged),
+ PreferenceType.int => _buildNumberEditor(textController, 'Integer value', true),
+ PreferenceType.double => _buildNumberEditor(textController, 'Double value', false),
+ PreferenceType.string => _buildTextEditor(textController),
+ PreferenceType.listString => _buildListEditor(listItems, textController, onListChanged),
+ PreferenceType.unknown => Text('Cannot edit unknown type'),
+ };
+ }
+
+ Widget _buildBoolEditor(bool value, Function(bool) onChanged) {
+ return CheckboxListTile(
+ title: Text('Value'),
+ value: value,
+ onChanged: (newValue) {
+ if (newValue != null) onChanged(newValue);
+ },
+ );
+ }
+
+ Widget _buildTextEditor(TextEditingController controller) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ TextField(
+ controller: controller,
+ decoration: InputDecoration(labelText: 'String value'),
+ maxLines: null,
+ ),
+ ],
+ );
+ }
+
+ Widget _buildNumberEditor(TextEditingController controller, String label, bool isInteger) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ TextField(
+ controller: controller,
+ decoration: InputDecoration(labelText: label),
+ keyboardType: isInteger
+ ? TextInputType.number
+ : TextInputType.numberWithOptions(decimal: true),
+ inputFormatters: isInteger
+ ? [FilteringTextInputFormatter.digitsOnly]
+ : [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*$'))],
+ ),
+ ],
+ );
+ }
+
+ Widget _buildListEditor(
+ List items,
+ TextEditingController controller,
+ Function(List) onListChanged,
+ ) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ SizedBox(
+ height: 200,
+ child: ReorderableListView(
+ shrinkWrap: true,
+ children: [
+ for (int i = 0; i < items.length; i++)
+ ListTile(
+ key: Key('$i'),
+ title: Text(items[i]),
+ trailing: IconButton(
+ icon: Icon(Icons.delete),
+ onPressed: () {
+ final newList = List.from(items);
+ newList.removeAt(i);
+ onListChanged(newList);
+ },
+ ),
+ )
+ ],
+ onReorder: (int oldIndex, int newIndex) {
+ final newList = List.from(items);
+ if (oldIndex < newIndex) {
+ newIndex -= 1;
+ }
+ final item = newList.removeAt(oldIndex);
+ newList.insert(newIndex, item);
+ onListChanged(newList);
+ },
+ ),
+ ),
+ Row(
+ children: [
+ Expanded(
+ child: TextField(
+ controller: controller,
+ decoration: InputDecoration(labelText: 'New item'),
+ ),
+ ),
+ IconButton(
+ icon: Icon(Icons.add),
+ onPressed: () {
+ if (controller.text.isNotEmpty) {
+ final newList = List.from(items);
+ newList.add(controller.text);
+ onListChanged(newList);
+ controller.clear();
+ }
+ },
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+
+ bool _validateAndUpdateValue(
+ BuildContext context,
+ PreferenceType type,
+ TextEditingController controller,
+ List listItems,
+ Function(dynamic) setNewValue,
+ ) {
+ switch (type) {
+ case PreferenceType.int:
+ if (controller.text.isNotEmpty) {
+ try {
+ setNewValue(int.parse(controller.text));
+ } catch (e) {
+ _showErrorMessage(context, 'Invalid integer value');
+ return false;
+ }
+ }
+ break;
+ case PreferenceType.double:
+ if (controller.text.isNotEmpty) {
+ try {
+ setNewValue(double.parse(controller.text));
+ } catch (e) {
+ _showErrorMessage(context, 'Invalid double value');
+ return false;
+ }
+ }
+ break;
+ case PreferenceType.string:
+ setNewValue(controller.text);
+ break;
+ case PreferenceType.listString:
+ setNewValue(listItems);
+ break;
+ default:
+ break;
+ }
+ return true;
+ }
+
+ void _showErrorMessage(BuildContext context, String message) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text(message)),
+ );
+ }
+
+ void _showCreateDialog(BuildContext context) {
+ PreferenceType selectedType = PreferenceType.string;
+ TextEditingController keyController = TextEditingController();
+
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return StatefulBuilder(
+ builder: (context, setState) {
+ return AlertDialog(
+ title: Text('Create Preference'),
+ content: SingleChildScrollView(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ TextField(
+ controller: keyController,
+ decoration: InputDecoration(labelText: 'Preference Key'),
+ ),
+ SizedBox(height: 16),
+ DropdownButtonFormField(
+ value: selectedType,
+ decoration: InputDecoration(labelText: 'Type'),
+ items: [
+ DropdownMenuItem(value: PreferenceType.string, child: Text('String')),
+ DropdownMenuItem(value: PreferenceType.bool, child: Text('Boolean')),
+ DropdownMenuItem(value: PreferenceType.int, child: Text('Integer')),
+ DropdownMenuItem(value: PreferenceType.double, child: Text('Double')),
+ DropdownMenuItem(value: PreferenceType.listString, child: Text('List of Strings')),
+ ],
+ onChanged: (value) {
+ if (value != null) {
+ setState(() {
+ selectedType = value;
+ });
+ }
+ },
+ ),
+ ],
+ ),
+ ),
+ actions: [
+ TextButton(
+ child: Text('Cancel'),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ TextButton(
+ child: Text('Create'),
+ onPressed: () {
+ if (keyController.text.isEmpty) {
+ _showErrorMessage(context, 'Key cannot be empty');
+ return;
+ }
+
+ viewModel.set(keyController.text, selectedType, switch (selectedType) {
+ PreferenceType.bool => false,
+ PreferenceType.int => 0,
+ PreferenceType.double => 0.0,
+ PreferenceType.string => '',
+ PreferenceType.listString => [],
+ PreferenceType.unknown => null,
+ });
+ Navigator.of(context).pop();
+ },
+ ),
+ ],
+ );
+ },
+ );
+ },
+ );
+ }
+}
diff --git a/lib/src/screens/settings/background_sync_page.dart b/lib/src/screens/settings/background_sync_page.dart
index f9589297d..91cab896b 100644
--- a/lib/src/screens/settings/background_sync_page.dart
+++ b/lib/src/screens/settings/background_sync_page.dart
@@ -12,6 +12,7 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/settings/sync_mode.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
+import 'package:permission_handler/permission_handler.dart';
class BackgroundSyncPage extends BasePage {
BackgroundSyncPage(this.dashboardViewModel);
@@ -28,30 +29,30 @@ class BackgroundSyncPage extends BasePage {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- if (dashboardViewModel.hasBatteryOptimization)
- Observer(builder: (context) {
- return SettingsSwitcherCell(
- title: S.current.unrestricted_background_service,
- value: !dashboardViewModel.batteryOptimizationEnabled,
- onValueChange: (_, bool value) {
- dashboardViewModel.disableBatteryOptimization();
- },
- );
- }),
Observer(builder: (context) {
return SettingsSwitcherCell(
title: S.current.background_sync,
value: dashboardViewModel.backgroundSyncEnabled,
- onValueChange: (dashboardViewModel.batteryOptimizationEnabled && dashboardViewModel.hasBatteryOptimization) ? (_, bool value) {
- unawaited(showPopUp(context: context, builder: (context) => AlertWithOneAction(
- alertTitle: S.current.background_sync,
- alertContent: S.current.unrestricted_background_service_notice,
- buttonText: S.current.ok,
- buttonAction: () => Navigator.of(context).pop(),
- )));
- } : (_, bool value) {
+ onValueChange: (_, bool value) async {
if (value) {
- dashboardViewModel.enableBackgroundSync();
+ if (dashboardViewModel.batteryOptimizationEnabled) {
+ await showPopUp(context: context, builder: (context) => AlertWithOneAction(
+ alertTitle: S.current.background_sync,
+ alertContent: S.current.unrestricted_background_service_notice,
+ buttonText: S.current.ok,
+ buttonAction: () => Navigator.of(context).pop(),
+ ));
+ await dashboardViewModel.disableBatteryOptimization();
+ for (var i = 0; i < 4 * 60; i++) {
+ await Future.delayed(Duration(milliseconds: 250));
+ if (!dashboardViewModel.batteryOptimizationEnabled) {
+ await dashboardViewModel.enableBackgroundSync();
+ return;
+ }
+ }
+ } else {
+ dashboardViewModel.enableBackgroundSync();
+ }
} else {
dashboardViewModel.disableBackgroundSync();
}
@@ -68,22 +69,58 @@ class BackgroundSyncPage extends BasePage {
dashboardViewModel.setSyncMode(syncMode);
});
}),
-
- // Observer(builder: (context) {
- // return SettingsSwitcherCell(
- // title: S.current.background_sync_on_battery,
- // value: dashboardViewModel.backgroundSyncOnBattery,
- // onValueChange: (_, bool value) =>
- // dashboardViewModel.setBackgroundSyncOnBattery(value),
- // );
- // }),
- // Observer(builder: (context) {
- // return SettingsSwitcherCell(
- // title: S.current.background_sync_on_data,
- // value: dashboardViewModel.backgroundSyncOnData,
- // onValueChange: (_, bool value) => dashboardViewModel.setBackgroundSyncOnData(value),
- // );
- // }),
+ if (dashboardViewModel.hasBgsyncNetworkConstraints)
+ Observer(builder: (context) {
+ return SettingsSwitcherCell(
+ title: S.current.background_sync_on_unmetered_network,
+ value: dashboardViewModel.backgroundSyncNetworkUnmetered,
+ onValueChange: (_, bool value) => dashboardViewModel.setBackgroundSyncNetworkUnmetered(value),
+ );
+ }),
+ if (dashboardViewModel.hasBgsyncBatteryNotLowConstraints)
+ Observer(builder: (context) {
+ return SettingsSwitcherCell(
+ title: S.current.background_sync_on_battery_low,
+ value: !dashboardViewModel.backgroundSyncBatteryNotLow,
+ onValueChange: (_, bool value) => dashboardViewModel.setBackgroundSyncBatteryNotLow(!value),
+ );
+ }),
+ if (dashboardViewModel.hasBgsyncChargingConstraints)
+ Observer(builder: (context) {
+ return SettingsSwitcherCell(
+ title: S.current.background_sync_on_charging,
+ value: dashboardViewModel.backgroundSyncCharging,
+ onValueChange: (_, bool value) => dashboardViewModel.setBackgroundSyncCharging(value),
+ );
+ }),
+ if (dashboardViewModel.hasBgsyncDeviceIdleConstraints)
+ Observer(builder: (context) {
+ return SettingsSwitcherCell(
+ title: S.current.background_sync_on_device_idle,
+ value: dashboardViewModel.backgroundSyncDeviceIdle,
+ onValueChange: (_, bool value) => dashboardViewModel.setBackgroundSyncDeviceIdle(value),
+ );
+ }),
+ Observer(builder: (context) {
+ return SettingsSwitcherCell(
+ title: S.current.new_transactions_notifications,
+ value: dashboardViewModel.backgroundSyncNotificationsEnabled,
+ onValueChange: (_, bool value) {
+ try {
+ dashboardViewModel.setBackgroundSyncNotificationsEnabled(value);
+ } catch (e) {
+ showPopUp(context: context, builder: (context) => AlertWithOneAction(
+ alertTitle: S.current.error,
+ alertContent: S.current.notification_permission_denied,
+ buttonText: S.current.ok,
+ buttonAction: () {
+ Navigator.of(context).pop();
+ },
+ ));
+ }
+ },
+ );
+ }),
],
),
);
diff --git a/lib/src/screens/settings/connection_sync_page.dart b/lib/src/screens/settings/connection_sync_page.dart
index 739e01c55..098f80644 100644
--- a/lib/src/screens/settings/connection_sync_page.dart
+++ b/lib/src/screens/settings/connection_sync_page.dart
@@ -49,7 +49,7 @@ class ConnectionSyncPage extends BasePage {
title: S.current.manage_nodes,
handler: (context) => Navigator.of(context).pushNamed(Routes.manageNodes),
),
- if (dashboardViewModel.hasBackgroundSync && Platform.isAndroid && FeatureFlag.isBackgroundSyncEnabled) ...[
+ if (Platform.isAndroid && FeatureFlag.isBackgroundSyncEnabled) ...[
SettingsCellWithArrow(
title: S.current.background_sync,
handler: (context) => Navigator.of(context).pushNamed(Routes.backgroundSync),
diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart
index 08556eda9..c0bcca877 100644
--- a/lib/src/screens/settings/other_settings_page.dart
+++ b/lib/src/screens/settings/other_settings_page.dart
@@ -6,12 +6,10 @@ import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/settings/widgets/setting_priority_picker_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart';
-import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_version_cell.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart';
import 'package:cw_core/wallet_type.dart';
-import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@@ -77,6 +75,18 @@ class OtherSettingsPage extends BasePage {
handler: (BuildContext context) =>
Navigator.of(context).pushNamed(Routes.devMoneroCallProfiler),
),
+ if (FeatureFlag.hasDevOptions)
+ SettingsCellWithArrow(
+ title: '[dev] shared preferences',
+ handler: (BuildContext context) =>
+ Navigator.of(context).pushNamed(Routes.devSharedPreferences),
+ ),
+ if (FeatureFlag.hasDevOptions)
+ SettingsCellWithArrow(
+ title: '[dev] background sync logs',
+ handler: (BuildContext context) =>
+ Navigator.of(context).pushNamed(Routes.devBackgroundSyncLogs),
+ ),
Spacer(),
SettingsVersionCell(
title: S.of(context).version(_otherSettingsViewModel.currentVersion)),
diff --git a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart
index d44107939..43e019360 100644
--- a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart
+++ b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart
@@ -97,6 +97,7 @@ class UnspentCoinsListFormState extends State {
void _showSavingDataAlert() {
showDialog(
context: context,
+ useRootNavigator: false,
builder: (BuildContext context) {
return AlertWithNoAction(
alertContent: 'Updating, please wait…',
diff --git a/lib/core/wallet_connect/models/bottom_sheet_queue_item_model.dart b/lib/src/screens/wallet_connect/models/bottom_sheet_queue_item_model.dart
similarity index 90%
rename from lib/core/wallet_connect/models/bottom_sheet_queue_item_model.dart
rename to lib/src/screens/wallet_connect/models/bottom_sheet_queue_item_model.dart
index 49eecac0f..44ee5e8a4 100644
--- a/lib/core/wallet_connect/models/bottom_sheet_queue_item_model.dart
+++ b/lib/src/screens/wallet_connect/models/bottom_sheet_queue_item_model.dart
@@ -6,11 +6,13 @@ class BottomSheetQueueItemModel {
final Widget widget;
final bool isModalDismissible;
final Completer completer;
+ final int closeAfter;
BottomSheetQueueItemModel({
required this.widget,
required this.completer,
this.isModalDismissible = false,
+ this.closeAfter = 0,
});
@override
diff --git a/lib/core/wallet_connect/models/connection_model.dart b/lib/src/screens/wallet_connect/models/wc_connection_model.dart
similarity index 60%
rename from lib/core/wallet_connect/models/connection_model.dart
rename to lib/src/screens/wallet_connect/models/wc_connection_model.dart
index 63cc8260f..f4cbb5723 100644
--- a/lib/core/wallet_connect/models/connection_model.dart
+++ b/lib/src/screens/wallet_connect/models/wc_connection_model.dart
@@ -1,10 +1,10 @@
-class ConnectionModel {
+class WCConnectionModel {
final String? title;
final String? text;
final List? elements;
final Map? elementActions;
- ConnectionModel({
+ WCConnectionModel({
this.title,
this.text,
this.elements,
@@ -13,6 +13,6 @@ class ConnectionModel {
@override
String toString() {
- return 'WalletConnectRequestModel(title: $title, text: $text, elements: $elements, elementActions: $elementActions)';
+ return 'WCConnectionModel(title: $title, text: $text, elements: $elements, elementActions: $elementActions)';
}
}
diff --git a/lib/core/wallet_connect/wc_bottom_sheet_service.dart b/lib/src/screens/wallet_connect/services/bottom_sheet_service.dart
similarity index 53%
rename from lib/core/wallet_connect/wc_bottom_sheet_service.dart
rename to lib/src/screens/wallet_connect/services/bottom_sheet_service.dart
index 3da8660f0..44637ac1b 100644
--- a/lib/core/wallet_connect/wc_bottom_sheet_service.dart
+++ b/lib/src/screens/wallet_connect/services/bottom_sheet_service.dart
@@ -1,19 +1,24 @@
import 'dart:async';
-import 'package:cake_wallet/core/wallet_connect/models/bottom_sheet_queue_item_model.dart';
+import 'dart:collection';
+import 'package:cake_wallet/src/screens/wallet_connect/models/bottom_sheet_queue_item_model.dart';
import 'package:flutter/material.dart';
+enum WCBottomSheetResult { reject, one, all }
+
abstract class BottomSheetService {
abstract final ValueNotifier currentSheet;
Future queueBottomSheet({
required Widget widget,
bool isModalDismissible = false,
+ int closeAfter = 0,
});
- void resetCurrentSheet();
+ void showNext();
}
class BottomSheetServiceImpl implements BottomSheetService {
+ Queue queue = Queue();
@override
final ValueNotifier currentSheet = ValueNotifier(null);
@@ -21,6 +26,7 @@ class BottomSheetServiceImpl implements BottomSheetService {
@override
Future queueBottomSheet({
required Widget widget,
+ int closeAfter = 0,
bool isModalDismissible = false,
}) async {
// Create the bottom sheet queue item
@@ -28,16 +34,28 @@ class BottomSheetServiceImpl implements BottomSheetService {
final queueItem = BottomSheetQueueItemModel(
widget: widget,
completer: completer,
+ closeAfter: closeAfter,
isModalDismissible: isModalDismissible,
);
- currentSheet.value = queueItem;
+ // If the current sheet it null, set it to the queue item
+ if (currentSheet.value == null) {
+ currentSheet.value = queueItem;
+ } else {
+ // Otherwise, add it to the queue
+ queue.add(queueItem);
+ }
+ // Return the future
return await completer.future;
}
@override
- void resetCurrentSheet() {
- currentSheet.value = null;
+ void showNext() {
+ if (queue.isEmpty) {
+ currentSheet.value = null;
+ } else {
+ currentSheet.value = queue.removeFirst();
+ }
}
}
diff --git a/lib/core/wallet_connect/chain_service/eth/evm_chain_id.dart b/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_id.dart
similarity index 78%
rename from lib/core/wallet_connect/chain_service/eth/evm_chain_id.dart
rename to lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_id.dart
index 0be21b1b2..6ecd09d35 100644
--- a/lib/core/wallet_connect/chain_service/eth/evm_chain_id.dart
+++ b/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_id.dart
@@ -1,5 +1,3 @@
-import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_service.dart';
-
enum EVMChainId {
ethereum,
polygon,
@@ -30,6 +28,6 @@ extension EVMChainIdX on EVMChainId {
break;
}
- return '${EvmChainServiceImpl.namespace}:$name';
+ return 'eip155:$name';
}
}
diff --git a/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart b/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart
new file mode 100644
index 000000000..05c2a1f55
--- /dev/null
+++ b/lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart
@@ -0,0 +1,581 @@
+import 'dart:convert';
+
+import 'package:cake_wallet/generated/i18n.dart';
+import 'package:cake_wallet/reactions/wallet_connect.dart';
+import 'package:eth_sig_util/eth_sig_util.dart';
+import 'package:eth_sig_util/util/utils.dart';
+import 'package:flutter/material.dart';
+import 'package:http/http.dart' as http;
+import 'package:reown_walletkit/reown_walletkit.dart';
+
+import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/eth/evm_chain_id.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/eth/evm_supported_methods.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/models/wc_connection_model.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/utils/eth_utils.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/utils/method_utils.dart';
+import 'package:cake_wallet/store/app_store.dart';
+import 'package:cake_wallet/.secrets.g.dart' as secrets;
+
+class EvmChainServiceImpl {
+ Map get sessionRequestHandlers => {
+ EVMSupportedMethods.ethSign.name: ethSign,
+ EVMSupportedMethods.ethSignTransaction.name: ethSignTransaction,
+ EVMSupportedMethods.ethSignTypedData.name: ethSignTypedData,
+ EVMSupportedMethods.ethSignTypedDataV4.name: ethSignTypedDataV4,
+ };
+
+ Map get methodRequestHandlers => {
+ EVMSupportedMethods.personalSign.name: personalSign,
+ EVMSupportedMethods.ethSendTransaction.name: ethSendTransaction,
+ };
+
+ EvmChainServiceImpl({
+ required this.reference,
+ required this.appStore,
+ required this.wcKeyService,
+ required this.bottomSheetService,
+ required this.walletKit,
+ Web3Client? web3Client,
+ }) : ethClient = web3Client ??
+ Web3Client(
+ appStore.settingsStore.getCurrentNode(appStore.wallet!.type).uri.toString(),
+ http.Client(),
+ ) {
+ for (final event in EventsConstants.allEvents) {
+ walletKit.registerEventEmitter(
+ chainId: getChainId(),
+ event: event,
+ );
+ }
+
+ for (var handler in methodRequestHandlers.entries) {
+ walletKit.registerRequestHandler(
+ chainId: getChainId(),
+ method: handler.key,
+ handler: handler.value,
+ );
+ }
+ for (var handler in sessionRequestHandlers.entries) {
+ walletKit.registerRequestHandler(
+ chainId: getChainId(),
+ method: handler.key,
+ handler: handler.value,
+ );
+ }
+
+ walletKit.onSessionRequest.subscribe(_onSessionRequest);
+ }
+
+ final AppStore appStore;
+ final EVMChainId reference;
+ final Web3Client ethClient;
+ final ReownWalletKit walletKit;
+ final WalletConnectKeyService wcKeyService;
+ final BottomSheetService bottomSheetService;
+
+ String getChainId() => reference.chain();
+
+ Future personalSign(String topic, dynamic parameters) async {
+ debugPrint('personalSign request: $parameters');
+
+ final pRequest = walletKit.pendingRequests.getAll().last;
+ final address = EthUtils.getAddressFromSessionRequest(pRequest);
+ final data = EthUtils.getDataFromSessionRequest(pRequest);
+ final message = EthUtils.getUtf8Message(data.toString());
+ var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0');
+
+ final isApproved = await MethodsUtils.requestApproval(
+ message,
+ method: pRequest.method,
+ chainId: pRequest.chainId,
+ address: address,
+ transportType: pRequest.transportType.name,
+ verifyContext: pRequest.verifyContext,
+ );
+
+ if (isApproved) {
+ try {
+ // Load the private key
+ final keys = wcKeyService.getKeysForChain(appStore.wallet!);
+ final credentials = EthPrivateKey.fromHex(keys[0].privateKey);
+
+ final signature = credentials.signPersonalMessageToUint8List(
+ utf8.encode(message),
+ );
+ final signedTx = bytesToHex(signature, include0x: true);
+
+ isValidSignature(signedTx, message, credentials.address.hex);
+
+ response = response.copyWith(result: signedTx);
+ } catch (e) {
+ debugPrint('personalSign error $e');
+ final error = Errors.getSdkError(Errors.MALFORMED_REQUEST_PARAMS);
+ response = response.copyWith(
+ error: JsonRpcError(code: error.code, message: error.message),
+ );
+ }
+ } else {
+ final error = Errors.getSdkError(Errors.USER_REJECTED);
+ response = response.copyWith(
+ error: JsonRpcError(code: error.code, message: error.message),
+ );
+ }
+
+ _handleResponseForTopic(topic, response);
+ }
+
+ Future ethSign(String topic, dynamic parameters) async {
+ debugPrint('ethSign request: $parameters');
+
+ final pRequest = walletKit.pendingRequests.getAll().last;
+ final address = EthUtils.getAddressFromSessionRequest(pRequest);
+ final data = EthUtils.getDataFromSessionRequest(pRequest);
+ final message = EthUtils.getUtf8Message(data.toString());
+ var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0');
+
+ final isApproved = await MethodsUtils.requestApproval(
+ message,
+ method: pRequest.method,
+ chainId: pRequest.chainId,
+ address: address,
+ transportType: pRequest.transportType.name,
+ verifyContext: pRequest.verifyContext,
+ );
+
+ if (isApproved) {
+ try {
+ // Load the private key
+ final keys = wcKeyService.getKeysForChain(appStore.wallet!);
+ final credentials = EthPrivateKey.fromHex(keys[0].privateKey);
+
+ final signature = credentials.signPersonalMessageToUint8List(
+ utf8.encode(message),
+ );
+ final signedTx = bytesToHex(signature, include0x: true);
+
+ isValidSignature(signedTx, message, credentials.address.hex);
+
+ response = response.copyWith(result: signedTx);
+ } catch (e) {
+ debugPrint('ethSign error $e');
+ final error = Errors.getSdkError(Errors.MALFORMED_REQUEST_PARAMS);
+ response = response.copyWith(
+ error: JsonRpcError(code: error.code, message: error.message),
+ );
+ }
+ } else {
+ final error = Errors.getSdkError(Errors.USER_REJECTED).toSignError();
+ response = response.copyWith(
+ error: JsonRpcError(code: error.code, message: error.message),
+ );
+ }
+
+ _handleResponseForTopic(topic, response);
+ }
+
+ Future ethSignTypedData(String topic, dynamic parameters) async {
+ debugPrint('ethSignTypedData request: $parameters');
+
+ final pRequest = walletKit.pendingRequests.getAll().last;
+ final address = EthUtils.getAddressFromSessionRequest(pRequest);
+ final data = EthUtils.getDataFromSessionRequest(pRequest) as String;
+ var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0');
+
+ final isApproved = await MethodsUtils.requestApproval(
+ data,
+ method: pRequest.method,
+ chainId: pRequest.chainId,
+ address: address,
+ transportType: pRequest.transportType.name,
+ verifyContext: pRequest.verifyContext,
+ );
+
+ if (isApproved) {
+ try {
+ final keys = wcKeyService.getKeysForChain(appStore.wallet!);
+
+ final signature = EthSigUtil.signTypedData(
+ privateKey: keys[0].privateKey,
+ jsonData: data,
+ version: TypedDataVersion.V4,
+ );
+
+ response = response.copyWith(result: signature);
+ } catch (e) {
+ debugPrint('ethSignTypedData error $e');
+ final error = Errors.getSdkError(Errors.MALFORMED_REQUEST_PARAMS);
+ response = response.copyWith(
+ error: JsonRpcError(code: error.code, message: error.message),
+ );
+ }
+ } else {
+ final error = Errors.getSdkError(Errors.USER_REJECTED).toSignError();
+ response = response.copyWith(
+ error: JsonRpcError(code: error.code, message: error.message),
+ );
+ }
+
+ _handleResponseForTopic(topic, response);
+ }
+
+ Future ethSignTypedDataV4(String topic, dynamic parameters) async {
+ debugPrint('ethSignTypedDataV4 request: $parameters');
+
+ final permitRequestMessage = await extractPermitData(parameters);
+
+ final pRequest = walletKit.pendingRequests.getAll().last;
+ final address = EthUtils.getAddressFromSessionRequest(pRequest);
+ final data = EthUtils.getDataFromSessionRequest(pRequest) as String;
+ var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0');
+
+ final isApproved = await MethodsUtils.requestApproval(
+ permitRequestMessage,
+ method: pRequest.method,
+ chainId: pRequest.chainId,
+ address: address,
+ transportType: pRequest.transportType.name,
+ verifyContext: pRequest.verifyContext,
+ );
+
+ if (isApproved) {
+ try {
+ final keys = wcKeyService.getKeysForChain(appStore.wallet!);
+
+ final signature = EthSigUtil.signTypedData(
+ privateKey: keys[0].privateKey,
+ jsonData: data,
+ version: TypedDataVersion.V4,
+ );
+
+ response = response.copyWith(result: signature);
+ } catch (e) {
+ debugPrint('ethSignTypedDataV4 error $e');
+ final error = Errors.getSdkError(Errors.MALFORMED_REQUEST_PARAMS);
+ response = response.copyWith(
+ error: JsonRpcError(code: error.code, message: error.message),
+ );
+ }
+ } else {
+ response = response.copyWith(
+ error: JsonRpcError(code: 5002, message: S.current.user_rejected_method),
+ );
+ }
+
+ _handleResponseForTopic(topic, response);
+ }
+
+ Future ethSignTransaction(String topic, dynamic parameters) async {
+ debugPrint('ethSignTransaction request: $parameters');
+
+ final SessionRequest pRequest = walletKit.pendingRequests.getAll().last;
+ final data = EthUtils.getTransactionFromSessionRequest(pRequest);
+
+ if (data == null) return;
+
+ final address = EthUtils.getAddressFromSessionRequest(pRequest);
+ var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0');
+
+ final transaction = await _approveTransaction(
+ data,
+ method: pRequest.method,
+ chainId: pRequest.chainId,
+ address: address,
+ transportType: pRequest.transportType.name,
+ verifyContext: pRequest.verifyContext,
+ );
+
+ if (transaction is Transaction) {
+ try {
+ // Load the private key
+ final keys = wcKeyService.getKeysForChain(appStore.wallet!);
+ final credentials = EthPrivateKey.fromHex(keys[0].privateKey);
+
+ final chainId = getChainId().split(':').last;
+
+ final signature = await ethClient.signTransaction(
+ credentials,
+ transaction,
+ chainId: int.parse(chainId),
+ );
+
+ // Sign the transaction
+ final signedTx = bytesToHex(signature, include0x: true);
+ response = response.copyWith(result: signedTx);
+ } on RPCError catch (e) {
+ debugPrint('ethSignTransaction error $e');
+ response = response.copyWith(
+ error: JsonRpcError(code: e.errorCode, message: e.message),
+ );
+ } catch (e) {
+ debugPrint('ethSignTransaction error $e');
+ final error = Errors.getSdkError(Errors.MALFORMED_REQUEST_PARAMS);
+ response = response.copyWith(
+ error: JsonRpcError(code: error.code, message: error.message),
+ );
+ }
+ } else {
+ response = response.copyWith(error: transaction as JsonRpcError);
+ }
+
+ _handleResponseForTopic(topic, response);
+ }
+
+ Future ethSendTransaction(String topic, dynamic parameters) async {
+ debugPrint('ethSendTransaction request: $parameters');
+ final SessionRequest pRequest = walletKit.pendingRequests.getAll().last;
+
+ final data = EthUtils.getTransactionFromSessionRequest(pRequest);
+ if (data == null) return;
+
+ var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0');
+
+ final transaction = await _approveTransaction(
+ data,
+ method: pRequest.method,
+ chainId: pRequest.chainId,
+ transportType: pRequest.transportType.name,
+ verifyContext: pRequest.verifyContext,
+ );
+ if (transaction is Transaction) {
+ try {
+ // Load the private key
+ final keys = wcKeyService.getKeysForChain(appStore.wallet!);
+ final credentials = EthPrivateKey.fromHex(keys[0].privateKey);
+ final chainId = getChainId().split(':').last;
+
+ final signedTx = await ethClient.sendTransaction(
+ credentials,
+ transaction,
+ chainId: int.parse(chainId),
+ );
+
+ response = response.copyWith(result: signedTx);
+ } on RPCError catch (e) {
+ debugPrint('ethSendTransaction error $e');
+ response = response.copyWith(
+ error: JsonRpcError(code: e.errorCode, message: e.message),
+ );
+ } catch (e) {
+ debugPrint('ethSendTransaction error $e');
+ final error = Errors.getSdkError(Errors.MALFORMED_REQUEST_PARAMS);
+ response = response.copyWith(
+ error: JsonRpcError(code: error.code, message: error.message),
+ );
+ }
+ } else {
+ response = response.copyWith(error: transaction as JsonRpcError);
+ }
+
+ _handleResponseForTopic(topic, response);
+ }
+
+ void _handleResponseForTopic(String topic, JsonRpcResponse response) async {
+ final session = walletKit.sessions.get(topic);
+
+ try {
+ await walletKit.respondSessionRequest(
+ topic: topic,
+ response: response,
+ );
+ MethodsUtils.handleRedirect(
+ topic,
+ session!.peer.metadata.redirect,
+ response.error?.message,
+ response.error == null,
+ );
+ } on ReownSignError catch (error) {
+ MethodsUtils.handleRedirect(
+ topic,
+ session!.peer.metadata.redirect,
+ error.message,
+ );
+ }
+ }
+
+ Future _approveTransaction(
+ Map transactionJson, {
+ String? title,
+ String? method,
+ String? chainId,
+ String? address,
+ VerifyContext? verifyContext,
+ required String transportType,
+ }) async {
+ Transaction transaction = transactionJson.toTransaction();
+
+ final gasPrice = await ethClient.getGasPrice();
+ try {
+ final gasLimit = await ethClient.estimateGas(
+ sender: transaction.from,
+ to: transaction.to,
+ value: transaction.value,
+ data: transaction.data,
+ gasPrice: gasPrice,
+ );
+
+ transaction = transaction.copyWith(
+ gasPrice: gasPrice,
+ maxGas: gasLimit.toInt(),
+ );
+ } on RPCError catch (e) {
+ return JsonRpcError(code: e.errorCode, message: e.message);
+ }
+
+ final gweiGasPrice = (transaction.gasPrice?.getInWei ?? BigInt.zero) / BigInt.from(1000000000);
+
+ final amount = (transaction.value?.getInWei ?? BigInt.zero) / BigInt.from(1e18);
+
+ final txMessageText = '${S.current.value}: ${amount.toStringAsFixed(9)} ETH\n'
+ '${S.current.from}: ${transaction.from?.hex}\n'
+ '${S.current.to}: ${transaction.to?.hex}';
+
+ if (await MethodsUtils.requestApproval(
+ txMessageText,
+ title: title,
+ method: method,
+ chainId: chainId,
+ address: address,
+ transportType: transportType,
+ verifyContext: verifyContext,
+ extraModels: [
+ WCConnectionModel(
+ title: S.current.gas_price,
+ elements: ['${gweiGasPrice.toStringAsFixed(2)} GWEI'],
+ ),
+ ],
+ )) {
+ return transaction;
+ }
+
+ return JsonRpcError(code: 5002, message: S.current.user_rejected_method);
+ }
+
+ void _onSessionRequest(SessionRequestEvent? args) async {
+ if (args != null && args.chainId == getChainId()) {
+ debugPrint('_onSessionRequest ${args.toString()}');
+ final handler = sessionRequestHandlers[args.method];
+ if (handler != null) {
+ await handler(args.topic, args.params);
+ }
+ }
+ }
+
+ bool isValidSignature(String hexSignature, String message, String hexAddress) {
+ try {
+ debugPrint('isValidSignature: $hexSignature, $message, $hexAddress');
+ final recoveredAddress = EthSigUtil.recoverPersonalSignature(
+ signature: hexSignature,
+ message: utf8.encode(message),
+ );
+ debugPrint('recoveredAddress: $recoveredAddress');
+
+ final recoveredAddress2 = EthSigUtil.recoverSignature(
+ signature: hexSignature,
+ message: utf8.encode(message),
+ );
+ debugPrint('recoveredAddress2: $recoveredAddress2');
+
+ final isValid = recoveredAddress == hexAddress;
+ return isValid;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ Future extractPermitData(dynamic data) async {
+ if (data is List && data.length >= 2) {
+ final typedData = jsonDecode(data[1] as String) as Map;
+
+ // Extracting domain details.
+ final domain = typedData['domain'] ?? {} as Map;
+ final domainName = domain['name']?.toString() ?? '';
+ final verifyingContract = domain['verifyingContract']?.toString() ?? '';
+
+ final chainId = domain['chainId']?.toString() ?? '';
+ final chainName = getChainNameBasedOnWalletType(appStore.wallet!.type);
+
+ // Get the primary type.
+ final primaryType = typedData['primaryType']?.toString() ?? '';
+
+ // Extracting message details.
+ final message = typedData['message'] ?? {} as Map;
+ final details = message['details'] ?? {} as Map;
+ final amount = details['amount']?.toString() ?? '';
+ final expirationRaw = details['expiration']?.toString() ?? '';
+ final nonce = details['nonce']?.toString() ?? '';
+
+ final tokenAddress = details['token']?.toString() ?? '';
+ final token = await getTokenDetails(tokenAddress, chainName);
+
+ final spender = message['spender']?.toString() ?? '';
+ final sigDeadlineRaw = message['sigDeadline']?.toString() ?? '';
+
+ // Converting expiration and sigDeadline from Unix time (seconds) to DateTime.
+ DateTime? expirationDate;
+ DateTime? sigDeadlineDate;
+ try {
+ if (expirationRaw.isNotEmpty) {
+ final int expirationInt = int.parse(expirationRaw);
+ expirationDate = DateTime.fromMillisecondsSinceEpoch(expirationInt * 1000);
+ }
+ if (sigDeadlineRaw.isNotEmpty) {
+ final int sigDeadlineInt = int.parse(sigDeadlineRaw);
+ sigDeadlineDate = DateTime.fromMillisecondsSinceEpoch(sigDeadlineInt * 1000);
+ }
+ } catch (e) {
+ // Parsing failed; we leave dates as null.
+ }
+
+ final permitData = {
+ 'domainName': domainName,
+ 'chainId': chainId,
+ 'verifyingContract': verifyingContract,
+ 'primaryType': primaryType,
+ 'token': token,
+ 'amount': amount,
+ 'expiration': expirationDate,
+ 'nonce': nonce,
+ 'spender': spender,
+ 'sigDeadline': sigDeadlineDate,
+ };
+
+ return 'Domain: ${permitData['domainName']}'
+ 'Chain ID: ${permitData['chainId']}'
+ 'Verifying Contract: ${permitData['verifyingContract']}'
+ 'Primary Type: ${permitData['primaryType']}'
+ 'Token: ${permitData['token']}'
+ 'Expiration: ${permitData['expiration'] != null ? permitData['expiration'] : 'N/A'}'
+ 'Spender: ${permitData['spender']}'
+ 'Signature Deadline: ${permitData['sigDeadline'] != null ? permitData['sigDeadline'] : 'N/A'}';
+ }
+ return '';
+ }
+
+ Future getTokenDetails(String contractAddress, String chainName) async {
+ final uri = Uri.https(
+ 'deep-index.moralis.io',
+ '/api/v2.2/erc20/metadata',
+ {
+ "chain": chainName,
+ "addresses": contractAddress,
+ },
+ );
+
+ final response = await http.get(
+ uri,
+ headers: {
+ "Accept": "application/json",
+ "X-API-Key": secrets.moralisApiKey,
+ },
+ );
+
+ final decodedResponse = jsonDecode(response.body)[0] as Map;
+
+ final symbol = (decodedResponse['symbol'] ?? '') as String;
+
+ final name = decodedResponse['name'] ?? '';
+ return '$name ($symbol)';
+ }
+}
diff --git a/lib/src/screens/wallet_connect/services/chain_service/eth/evm_supported_methods.dart b/lib/src/screens/wallet_connect/services/chain_service/eth/evm_supported_methods.dart
new file mode 100644
index 000000000..32e00570c
--- /dev/null
+++ b/lib/src/screens/wallet_connect/services/chain_service/eth/evm_supported_methods.dart
@@ -0,0 +1,31 @@
+enum EVMSupportedMethods {
+ ethSign,
+ ethSignTransaction,
+ ethSignTypedData,
+ ethSignTypedDataV4,
+ switchChain,
+ addChain,
+ personalSign,
+ ethSendTransaction;
+
+ String get name {
+ switch (this) {
+ case ethSign:
+ return 'eth_sign';
+ case ethSignTransaction:
+ return 'eth_signTransaction';
+ case ethSignTypedData:
+ return 'eth_signTypedData';
+ case ethSignTypedDataV4:
+ return 'eth_signTypedData_v4';
+ case switchChain:
+ return 'wallet_switchEthereumChain';
+ case addChain:
+ return 'wallet_addEthereumChain';
+ case personalSign:
+ return 'personal_sign';
+ case ethSendTransaction:
+ return 'eth_sendTransaction';
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/src/screens/wallet_connect/services/chain_service/solana/solana_chain_id.dart b/lib/src/screens/wallet_connect/services/chain_service/solana/solana_chain_id.dart
new file mode 100644
index 000000000..c5a3c9d25
--- /dev/null
+++ b/lib/src/screens/wallet_connect/services/chain_service/solana/solana_chain_id.dart
@@ -0,0 +1,22 @@
+enum SolanaChainId { mainnet, devnet, testnet }
+
+extension SolanaChainIdX on SolanaChainId {
+ String chain() {
+ String name = '';
+
+ switch (this) {
+ case SolanaChainId.mainnet:
+ name = '4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ';
+ // '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp';
+ break;
+ case SolanaChainId.devnet:
+ name = 'EtWTRABZaYq6iMfeYKouRu166VU2xqa1';
+ break;
+ case SolanaChainId.testnet:
+ name = '4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z';
+ break;
+ }
+
+ return 'solana:$name';
+ }
+}
diff --git a/lib/src/screens/wallet_connect/services/chain_service/solana/solana_chain_service.dart b/lib/src/screens/wallet_connect/services/chain_service/solana/solana_chain_service.dart
new file mode 100644
index 000000000..076c63228
--- /dev/null
+++ b/lib/src/screens/wallet_connect/services/chain_service/solana/solana_chain_service.dart
@@ -0,0 +1,287 @@
+import 'dart:convert';
+
+import 'package:blockchain_utils/base58/base58.dart';
+import 'package:blockchain_utils/blockchain_utils.dart' as blockchain_utils;
+import 'package:cake_wallet/generated/i18n.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/solana/solana_supported_methods.dart';
+import 'package:flutter/material.dart';
+import 'package:on_chain/solana/solana.dart';
+import 'package:reown_walletkit/reown_walletkit.dart';
+
+import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/solana/solana_chain_id.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/utils/method_utils.dart';
+import 'package:cake_wallet/store/app_store.dart';
+
+class SolanaChainService {
+ Map get solanaRequestHandlers => {
+ SolanaSupportedMethods.solSignMessage.name: solanaSignMessage,
+ SolanaSupportedMethods.solSignTransaction.name: solanaSignTransaction,
+ SolanaSupportedMethods.solSignAllTransaction.name: solanaSignAllTransaction,
+ };
+
+ SolanaChainService({
+ required this.appStore,
+ required this.bottomSheetService,
+ required this.walletKit,
+ required this.wcKeyService,
+ required this.reference,
+ }) {
+ for (var handler in solanaRequestHandlers.entries) {
+ walletKit.registerRequestHandler(
+ chainId: getChainId(),
+ method: handler.key,
+ handler: handler.value,
+ );
+ }
+ }
+
+ final AppStore appStore;
+ final BottomSheetService bottomSheetService;
+ final ReownWalletKit walletKit;
+ final WalletConnectKeyService wcKeyService;
+ final SolanaChainId reference;
+
+ String getChainId() => reference.chain();
+
+ Future solanaSignMessage(String topic, dynamic parameters) async {
+ debugPrint('solanaSignMessage request: $parameters');
+
+ final pRequest = walletKit.pendingRequests.getAll().last;
+ var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0');
+
+ try {
+ final params = parameters as Map;
+ final message = params['message'].toString();
+
+ final privateKey = _getSolanaPrivateKey();
+
+ // it's sent as base58 encoded from the dapp
+ final base58Decoded = base58.decode(message);
+ final decodedMessage = utf8.decode(base58Decoded);
+
+ final isApproved = await MethodsUtils.requestApproval(
+ decodedMessage,
+ method: pRequest.method,
+ chainId: pRequest.chainId,
+ address: privateKey.publicKey().toAddress().address,
+ transportType: pRequest.transportType.name,
+ );
+
+ if (isApproved) {
+ final signedBytes = await privateKey.sign(base58Decoded);
+
+ final signature = blockchain_utils.Base58Encoder.encode(signedBytes);
+
+ response = response.copyWith(result: {'signature': signature});
+ } else {
+ final error = Errors.getSdkError(Errors.USER_REJECTED);
+ response = response.copyWith(
+ error: JsonRpcError(code: error.code, message: error.message),
+ );
+ }
+ //
+ } catch (e) {
+ debugPrint('solanaSignMessage error $e');
+ final error = Errors.getSdkError(Errors.MALFORMED_REQUEST_PARAMS);
+
+ response = response.copyWith(
+ error: JsonRpcError(code: error.code, message: error.message),
+ );
+ }
+
+ await walletKit.respondSessionRequest(topic: topic, response: response);
+
+ _handleResponseForTopic(topic, response);
+ }
+
+ Future solanaSignTransaction(String topic, dynamic parameters) async {
+ debugPrint('solanaSignTransaction: ${jsonEncode(parameters)}');
+
+ final pRequest = walletKit.pendingRequests.getAll().last;
+ var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0');
+
+ try {
+ final params = parameters as Map;
+ final privateKey = _getSolanaPrivateKey();
+
+ final beautifiedTrx = const JsonEncoder.withIndent(' ').convert(params);
+
+ SolanaTransaction unSignedTransaction;
+ if (params.containsKey('transaction')) {
+ final transaction = params['transaction'] as String;
+ final transactionBytes = base64.decode(transaction);
+ unSignedTransaction = SolanaTransaction.deserialize(transactionBytes);
+ } else {
+ final feePayer = params['feePayer'].toString();
+ final recentBlockHash = params['recentBlockhash'].toString();
+ final instructionsList = params['instructions'] as List;
+
+ final instructions = instructionsList.map((json) {
+ return (json as Map).toInstruction();
+ }).toList();
+
+ unSignedTransaction = SolanaTransaction(
+ payerKey: SolAddress(feePayer),
+ instructions: instructions,
+ recentBlockhash: SolAddress(recentBlockHash),
+ );
+ }
+
+ final isApproved = await MethodsUtils.requestApproval(
+ beautifiedTrx,
+ method: pRequest.method,
+ chainId: pRequest.chainId,
+ address: privateKey.publicKey().toAddress().address,
+ transportType: pRequest.transportType.name,
+ );
+
+ if (isApproved) {
+ final signedTx = await privateKey.sign(unSignedTransaction.serializeMessage());
+
+ final signature = Base58Encoder.encode(signedTx.toList(growable: false));
+
+ response = response.copyWith(result: {'signature': signature});
+ } else {
+ final error = Errors.getSdkError(Errors.USER_REJECTED);
+ response = response.copyWith(
+ error: JsonRpcError(code: error.code, message: error.message),
+ );
+ }
+ } catch (e, s) {
+ debugPrint('solanaSignTransaction error $e, $s');
+ final error = Errors.getSdkError(Errors.MALFORMED_REQUEST_PARAMS);
+
+ response = response.copyWith(
+ error: JsonRpcError(code: error.code, message: error.message),
+ );
+ }
+
+ await walletKit.respondSessionRequest(topic: topic, response: response);
+
+ _handleResponseForTopic(topic, response);
+ }
+
+ Future solanaSignAllTransaction(String topic, dynamic parameters) async {
+ debugPrint('solanaSignAllTransaction: ${jsonEncode(parameters)}');
+
+ final pRequest = walletKit.pendingRequests.getAll().last;
+ var response = JsonRpcResponse(id: pRequest.id, jsonrpc: '2.0');
+
+ try {
+ final params = parameters as Map;
+ final beautifiedTrx = const JsonEncoder.withIndent(' ').convert(params);
+
+ final privateKey = _getSolanaPrivateKey();
+
+ final isApproved = await MethodsUtils.requestApproval(
+ beautifiedTrx,
+ method: pRequest.method,
+ chainId: pRequest.chainId,
+ address: privateKey.publicKey().toAddress().address,
+ transportType: pRequest.transportType.name,
+ );
+
+ if (isApproved) {
+ if (params.containsKey('transactions')) {
+ final transactions = params['transactions'] as List;
+
+ List signedTransactions = [];
+ for (var transaction in transactions) {
+ final transactionBytes = base64.decode(transaction);
+
+ final unsignedTx = SolanaTransaction.deserialize(transactionBytes);
+
+ final serializedTx = await privateKey.sign(unsignedTx.serializeMessage());
+
+ unsignedTx.addSignature(privateKey.publicKey().toAddress(), serializedTx);
+
+ final reEncodedTx = unsignedTx.serializeString(
+ encoding: TransactionSerializeEncoding.base64,
+ );
+
+ signedTransactions.add(reEncodedTx);
+ }
+
+ response = response.copyWith(result: {'transactions': signedTransactions});
+ }
+ } else {
+ final error = Errors.getSdkError(Errors.USER_REJECTED);
+ response = response.copyWith(
+ error: JsonRpcError(code: error.code, message: error.message),
+ );
+ }
+ } catch (e, s) {
+ debugPrint('solanaSignAllTransactions error $e, $s');
+ final error = Errors.getSdkError(Errors.MALFORMED_REQUEST_PARAMS);
+
+ response = response.copyWith(
+ error: JsonRpcError(code: error.code, message: error.message),
+ );
+ }
+
+ await walletKit.respondSessionRequest(topic: topic, response: response);
+
+ _handleResponseForTopic(topic, response);
+ }
+
+ SolanaPrivateKey _getSolanaPrivateKey() {
+ final keys = wcKeyService.getKeysForChain(appStore.wallet!);
+
+ return SolanaPrivateKey.fromSeedHex(keys[0].privateKey);
+ }
+
+ void _handleResponseForTopic(String topic, JsonRpcResponse response) async {
+ final session = walletKit.sessions.get(topic);
+
+ try {
+ await walletKit.respondSessionRequest(topic: topic, response: response);
+
+ MethodsUtils.handleRedirect(
+ topic,
+ session!.peer.metadata.redirect,
+ response.error?.message,
+ );
+ } on ReownSignError catch (error) {
+ if (error.message.contains('No matching key')) {
+ MethodsUtils.handleRedirect(
+ topic,
+ session!.peer.metadata.redirect,
+ '${S.current.error_while_processing} ${S.current.youCanGoBackToYourDapp}',
+ );
+ } else {
+ MethodsUtils.handleRedirect(
+ topic,
+ session!.peer.metadata.redirect,
+ error.message,
+ );
+ }
+ }
+ }
+}
+
+extension on Map {
+ TransactionInstruction toInstruction() {
+ final programId = this['programId'] as String;
+
+ final data = (this['data'] as List).map((e) => e as int).toList();
+ // final data58 = base58.encode(Uint8List.fromList(data));
+ // final dataBytes = ByteArray.fromBase58(data58);
+
+ final keys = this['keys'] as List;
+ return TransactionInstruction.fromBytes(
+ programId: SolAddress(programId),
+ instructionBytes: data,
+ keys: keys.map((k) {
+ final kParams = (k as Map);
+ return AccountMeta(
+ publicKey:
+ SolanaPublicKey.fromBytes(base58.decode(kParams['pubkey'] as String)).toAddress(),
+ isWritable: kParams['isWritable'] as bool,
+ isSigner: kParams['isSigner'] as bool,
+ );
+ }).toList(),
+ );
+ }
+}
diff --git a/lib/src/screens/wallet_connect/services/chain_service/solana/solana_supported_methods.dart b/lib/src/screens/wallet_connect/services/chain_service/solana/solana_supported_methods.dart
new file mode 100644
index 000000000..c51482d3a
--- /dev/null
+++ b/lib/src/screens/wallet_connect/services/chain_service/solana/solana_supported_methods.dart
@@ -0,0 +1,16 @@
+enum SolanaSupportedMethods {
+ solSignMessage,
+ solSignTransaction,
+ solSignAllTransaction;
+
+ String get name {
+ switch (this) {
+ case solSignMessage:
+ return 'solana_signMessage';
+ case solSignTransaction:
+ return 'solana_signTransaction';
+ case solSignAllTransaction:
+ return 'solana_signAllTransactions';
+ }
+ }
+}
diff --git a/lib/src/screens/wallet_connect/services/key_service/chain_key_model.dart b/lib/src/screens/wallet_connect/services/key_service/chain_key_model.dart
new file mode 100644
index 000000000..e85c31ac3
--- /dev/null
+++ b/lib/src/screens/wallet_connect/services/key_service/chain_key_model.dart
@@ -0,0 +1,37 @@
+import 'dart:convert';
+
+class ChainKeyModel {
+ final List chains;
+ final String privateKey;
+ final String publicKey;
+
+ ChainKeyModel({
+ required this.chains,
+ required this.privateKey,
+ required this.publicKey,
+ });
+
+ String get namespace {
+ if (chains.isNotEmpty) {
+ return chains.first.split(':').first;
+ }
+ return '';
+ }
+
+ Map toJson() => {
+ 'chains': chains,
+ 'privateKey': privateKey,
+ 'publicKey': privateKey,
+ };
+
+ factory ChainKeyModel.fromJson(Map json) {
+ return ChainKeyModel(
+ chains: (json['chains'] as List).map((e) => '$e').toList(),
+ privateKey: json['privateKey'] as String,
+ publicKey: json['publicKey'] as String,
+ );
+ }
+
+ @override
+ String toString() => jsonEncode(toJson());
+}
diff --git a/lib/core/wallet_connect/wallet_connect_key_service.dart b/lib/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart
similarity index 96%
rename from lib/core/wallet_connect/wallet_connect_key_service.dart
rename to lib/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart
index f05adad97..a7f40455c 100644
--- a/lib/core/wallet_connect/wallet_connect_key_service.dart
+++ b/lib/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart
@@ -1,8 +1,8 @@
import 'package:cake_wallet/ethereum/ethereum.dart';
-import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/solana/solana.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/chain_key_model.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
diff --git a/lib/src/screens/wallet_connect/services/walletkit_service.dart b/lib/src/screens/wallet_connect/services/walletkit_service.dart
new file mode 100644
index 000000000..9b48cd233
--- /dev/null
+++ b/lib/src/screens/wallet_connect/services/walletkit_service.dart
@@ -0,0 +1,623 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:cw_core/wallet_type.dart';
+import 'package:eth_sig_util/util/utils.dart';
+import 'package:flutter/material.dart';
+import 'package:mobx/mobx.dart';
+import 'package:reown_walletkit/reown_walletkit.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+import 'package:cake_wallet/.secrets.g.dart' as secrets;
+import 'package:cake_wallet/entities/preferences_key.dart';
+import 'package:cake_wallet/generated/i18n.dart';
+import 'package:cake_wallet/reactions/wallet_connect.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/eth/evm_chain_id.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/chain_key_model.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/utils/eth_utils.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/utils/method_utils.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_connection_request_widget.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_message_display_widget.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_request_widget.dart';
+import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_session_auth_request_widget.dart';
+import 'package:cake_wallet/store/app_store.dart';
+
+import 'bottom_sheet_service.dart';
+import 'chain_service/solana/solana_chain_id.dart';
+import 'chain_service/solana/solana_chain_service.dart';
+
+part 'walletkit_service.g.dart';
+
+class WalletKitService = WalletKitServiceBase with _$WalletKitService;
+
+abstract class WalletKitServiceBase with Store {
+ WalletKitServiceBase(
+ this._bottomSheetHandler,
+ this.walletKeyService,
+ this.appStore,
+ this.sharedPreferences,
+ ) : pairings = ObservableList(),
+ sessions = ObservableList(),
+ auth = ObservableList(),
+ isInitialized = false;
+
+ final AppStore appStore;
+ final SharedPreferences sharedPreferences;
+ final BottomSheetService _bottomSheetHandler;
+ final WalletConnectKeyService walletKeyService;
+
+ late ReownWalletKit _walletKit;
+
+ @observable
+ bool isInitialized;
+
+ /// The list of requests from the dapp
+ /// Potential types include, but aren't limited to:
+ /// [SessionProposalEvent], [SessionAuthRequest]
+ @observable
+ ObservableList pairings;
+
+ @observable
+ ObservableList sessions;
+
+ @observable
+ ObservableList auth;
+
+ @action
+ void create() {
+ // Create the walletkit client
+ _walletKit = ReownWalletKit(
+ core: ReownCore(
+ projectId: secrets.walletConnectProjectId,
+ ),
+ metadata: const PairingMetadata(
+ name: 'Cake Wallet',
+ description: 'Cake Wallet',
+ url: 'https://cakewallet.com',
+ icons: ['https://cakewallet.com/assets/image/cake_logo.png'],
+ ),
+ );
+
+ _walletKit.core.addLogListener(_logListener);
+
+ // Setup our listeners
+ log('Created instance of walletKit');
+
+ _walletKit.core.pairing.onPairingInvalid.subscribe(_onPairingInvalid);
+ _walletKit.core.pairing.onPairingCreate.subscribe(_onPairingCreate);
+ _walletKit.core.relayClient.onRelayClientError.subscribe(_onRelayClientError);
+ _walletKit.core.relayClient.onRelayClientMessage.subscribe(_onRelayClientMessage);
+
+ _walletKit.onSessionProposal.subscribe(_onSessionProposal);
+ _walletKit.onSessionProposalError.subscribe(_onSessionProposalError);
+ _walletKit.onSessionConnect.subscribe(_onSessionConnect);
+ _walletKit.onSessionAuthRequest.subscribe(_onSessionAuthRequest);
+
+ _walletKit.pairings.onSync.subscribe(_onPairingsSync);
+ _walletKit.core.pairing.onPairingDelete.subscribe(_onPairingDelete);
+ _walletKit.core.pairing.onPairingExpire.subscribe(_onPairingDelete);
+
+ // Setup our accounts
+ List chainKeys = walletKeyService.getKeys(appStore.wallet!);
+ for (final chainKey in chainKeys) {
+ for (final chainId in chainKey.chains) {
+ final chainNameSpace = getChainNameSpaceAndIdBasedOnWalletType(appStore.wallet!.type);
+ if (chainNameSpace == chainId) {
+ final account = '$chainId:${chainKey.publicKey}';
+ debugPrint('registerAccount $account');
+ _walletKit.registerAccount(
+ chainId: chainId,
+ accountAddress: chainKey.publicKey,
+ );
+ }
+ }
+ }
+ }
+
+ void _logListener(String event) {
+ debugPrint('[WalletKit] $event');
+ }
+
+ @action
+ Future init() async {
+ // Await the initialization of walletKit
+ debugPrint('Intializing walletKit');
+ if (!isInitialized) {
+ try {
+ await _walletKit.init();
+ debugPrint('Initialized');
+ isInitialized = true;
+ } catch (e) {
+ debugPrint('init Error: ${e.toString()}');
+ isInitialized = false;
+ }
+ }
+
+ await _emitEvent();
+
+ _refreshPairings();
+
+ final newSessions = _walletKit.sessions.getAll();
+ sessions.addAll(newSessions);
+
+ final newAuthRequests = _walletKit.sessionAuthRequests.getAll();
+ auth.addAll(newAuthRequests);
+
+ if (isEVMCompatibleChain(appStore.wallet!.type)) {
+ for (final cId in EVMChainId.values) {
+ EvmChainServiceImpl(
+ reference: cId,
+ appStore: appStore,
+ wcKeyService: walletKeyService,
+ bottomSheetService: _bottomSheetHandler,
+ walletKit: _walletKit,
+ );
+ }
+ }
+
+ if (appStore.wallet!.type == WalletType.solana) {
+ for (final cId in SolanaChainId.values) {
+ SolanaChainService(
+ reference: cId,
+ appStore: appStore,
+ wcKeyService: walletKeyService,
+ bottomSheetService: _bottomSheetHandler,
+ walletKit: _walletKit,
+ );
+ }
+ }
+ }
+
+ @action
+ Future _emitEvent() async {
+ final isOnline = _walletKit.core.connectivity.isOnline.value;
+ if (!isOnline) {
+ await Future.delayed(const Duration(milliseconds: 500));
+ _emitEvent();
+ return;
+ }
+
+ final sessions = _walletKit.sessions.getAll();
+ for (var session in sessions) {
+ final chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
+ for (var chain in chainKeys) {
+ for (var chainID in chain.chains) {
+ try {
+ final events = NamespaceUtils.getNamespacesEventsForChain(
+ chainId: chainID,
+ namespaces: session.namespaces,
+ );
+ if (events.contains('accountsChanged')) {
+ final chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
+ _walletKit.emitSessionEvent(
+ topic: session.topic,
+ chainId: chainID,
+ event: SessionEventParams(
+ name: 'accountsChanged',
+ data: [chainKeys.first.publicKey],
+ ),
+ );
+ }
+ } catch (_) {}
+ }
+ }
+ }
+ }
+
+ @action
+ FutureOr onDispose() {
+ log('walletKit dispose');
+ _walletKit.core.removeLogListener(_logListener);
+
+ _walletKit.core.pairing.onPairingInvalid.unsubscribe(_onPairingInvalid);
+ _walletKit.core.pairing.onPairingCreate.unsubscribe(_onPairingCreate);
+ _walletKit.core.relayClient.onRelayClientError.unsubscribe(_onRelayClientError);
+ _walletKit.core.relayClient.onRelayClientMessage.unsubscribe(_onRelayClientMessage);
+
+ _walletKit.onSessionProposal.unsubscribe(_onSessionProposal);
+ _walletKit.onSessionProposalError.unsubscribe(_onSessionProposalError);
+ _walletKit.onSessionConnect.unsubscribe(_onSessionConnect);
+ _walletKit.onSessionAuthRequest.unsubscribe(_onSessionAuthRequest);
+
+ _walletKit.pairings.onSync.unsubscribe(_onPairingsSync);
+ _walletKit.core.pairing.onPairingDelete.unsubscribe(_onPairingDelete);
+ _walletKit.core.pairing.onPairingExpire.unsubscribe(_onPairingDelete);
+
+ isInitialized = false;
+ }
+
+ ReownWalletKit get walletKit => _walletKit;
+
+ void _onRelayClientMessage(MessageEvent? event) async {
+ if (event != null) {
+ final jsonObject = await EthUtils.decodeMessageEvent(event);
+ debugPrint('_onRelayClientMessage $jsonObject');
+
+ if (jsonObject is JsonRpcRequest) {
+ debugPrint(jsonObject.id.toString());
+ debugPrint(jsonObject.method);
+
+ if (jsonObject.method == 'wc_sessionDelete') {
+ await disconnectSession(topic: event.topic);
+ }
+ }
+ }
+ }
+
+ void _onPairingsSync(StoreSyncEvent? args) {
+ if (args != null) {
+ _refreshPairings();
+ }
+ }
+
+ void _onPairingDelete(PairingEvent? event) {
+ _refreshPairings();
+ }
+
+ @action
+ Future _onSessionProposal(SessionProposalEvent? args) async {
+ debugPrint('_onSessionProposal ${jsonEncode(args?.params)}');
+
+ if (args != null) {
+ final proposer = args.params.proposer;
+ final result = (await _bottomSheetHandler.queueBottomSheet(
+ widget: WCRequestWidget(
+ verifyContext: args.verifyContext,
+ child: WCConnectionRequestWidget(
+ proposalData: args.params,
+ verifyContext: args.verifyContext,
+ requester: proposer,
+ walletKeyService: walletKeyService,
+ walletKit: walletKit,
+ appStore: appStore,
+ ),
+ ),
+ )) ??
+ WCBottomSheetResult.reject;
+
+ if (result != WCBottomSheetResult.reject) {
+ try {
+ await _walletKit.approveSession(
+ id: args.id,
+ namespaces: NamespaceUtils.regenerateNamespacesWithChains(
+ args.params.generatedNamespaces!,
+ ),
+ sessionProperties: args.params.sessionProperties,
+ );
+ } on ReownSignError catch (error) {
+ MethodsUtils.handleRedirect(
+ '',
+ proposer.metadata.redirect,
+ error.message,
+ );
+ }
+ } else {
+ final error = Errors.getSdkError(Errors.USER_REJECTED).toSignError();
+ await _walletKit.rejectSession(id: args.id, reason: error);
+ await _walletKit.core.pairing.disconnect(topic: args.params.pairingTopic);
+ MethodsUtils.handleRedirect(
+ '',
+ proposer.metadata.redirect,
+ error.message,
+ );
+ }
+ }
+ }
+
+ @action
+ Future _onSessionProposalError(SessionProposalErrorEvent? args) async {
+ debugPrint('_onSessionProposalError $args');
+
+ if (args != null) {
+ String errorMessage = args.error.message;
+ if (args.error.code == 5100) {
+ errorMessage =
+ errorMessage.replaceFirst('${S.current.requested}:', '\n\n${S.current.requested}:');
+ errorMessage =
+ errorMessage.replaceFirst('${S.current.supported}:', '\n\n${S.current.supported}:');
+ }
+ MethodsUtils.goBackModal(
+ title: S.current.error,
+ message: errorMessage,
+ success: false,
+ );
+ }
+ }
+
+ @action
+ Future _onSessionConnect(SessionConnect? args) async {
+ if (args != null) {
+ final session = jsonEncode(args.session.toJson());
+
+ debugPrint('_onSessionConnect $session');
+
+ await savePairingTopicToLocalStorage(args.session.pairingTopic);
+
+ sessions.add(args.session);
+
+ _refreshPairings();
+
+ MethodsUtils.handleRedirect(
+ args.session.topic,
+ args.session.peer.metadata.redirect,
+ '',
+ true,
+ );
+ }
+ }
+
+ @action
+ void _onRelayClientError(ErrorEvent? args) {
+ debugPrint('_onRelayClientError ${args?.error}');
+ // _bottomSheetHandler.queueBottomSheet(
+ // isModalDismissible: true,
+ // widget: BottomSheetMessageDisplayWidget(
+ // message: "WC RelayClient Error: ${args?.error}",
+ // ),
+ // );
+ }
+
+ @action
+ void _onPairingInvalid(PairingInvalidEvent? args) {
+ debugPrint('_onPairingInvalid $args');
+ _bottomSheetHandler.queueBottomSheet(
+ isModalDismissible: true,
+ widget: BottomSheetMessageDisplayWidget(
+ message: '${S.current.pairingInvalidEvent}: $args',
+ ),
+ );
+ }
+
+ @action
+ void _onPairingCreate(PairingEvent? args) {
+ debugPrint('_onPairingCreate $args');
+ }
+
+ Future _onSessionAuthRequest(SessionAuthRequest? args) async {
+ if (args != null) {
+ final SessionAuthPayload authPayload = args.authPayload;
+ final jsonPyaload = jsonEncode(authPayload.toJson());
+
+ debugPrint('_onSessionAuthRequest $jsonPyaload');
+
+ final chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
+ final supportedChains = chainKeys.first.chains;
+ final supportedMethods = getChainSupportedMethodsOnWalletType(appStore.wallet!.type);
+
+ final newAuthPayload = AuthSignature.populateAuthPayload(
+ authPayload: authPayload,
+ chains: supportedChains.toList(),
+ methods: supportedMethods.toList(),
+ );
+ final cacaoRequestPayload = CacaoRequestPayload.fromSessionAuthPayload(
+ newAuthPayload,
+ );
+
+ final List