Explore-Reown-For-WalletConnect (#2156)

* Reown WalletConnect Implementation
- Setup for Solana Chain
- Setup for EVM chain
- Enable signing messages and transactions on Solana Chain
- Fix issues related to signinig of transactions on Solana chain
- Decode mesage to human readable format - WIP

* Reown WalletConnect Implementation Continuation:
- Add readable text for signedTypedDatav4 and sendTransaction event requests
- Modify styling for bottomsheetModel
- Switch minimum deployment target to 13.0
- Gracefully handle errors
- Fix issues discovered while testing across dApps

* chore: ReownWalletConnectImplementation (Cont.)
- Remove unneeded code and files
- Rename files to match classes and vice versa
- Add maven jitpack repository for reown.com/yttrium repo
- Add better folder structuring to entire feature
- Add localization for all feature related texts
- Add testnet and devnet access for Solana Chain Service
- Add a better error message for Moralis NFT fetch

* feat:
- Add field to enter wc uri directly
- Handle bottomsheet not coming up after initial display
- Remove unneeded print statements
- Remove double display of success popup for session proposal bottomsheets

* fix:
- Add visual feedback to WC RelayClient Errors
- Adjust theme for bottomSheets

* Testing a theroy

* Temporarily add flags back

* Remove added flags and update proguard rules

* fix(android): update proguard-rules.pro and drop minify flags (#2199)

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* fix: Switch bottomsheet result from signOne to signAll

---------

Co-authored-by: cyan <cyjan@mrcyjanek.net>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
David Adegoke
2025-04-24 18:39:58 +02:00
committed by GitHub
co-authored by Omar Hatem cyan
parent 526796e2d7
commit e6c9cf54fb
85 changed files with 3453 additions and 1638 deletions
@@ -1,5 +0,0 @@
abstract class ChainService {
String getNamespace();
String getChainId();
List<String> getEvents();
}
@@ -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<String> getEvents() {
return ['chainChanged', 'accountsChanged'];
}
Future<String?> 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<String> 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<ChainKeyModel> 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<String> 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<ChainKeyModel> 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<String> ethSignTransaction(String topic, dynamic parameters) async {
log('received eth sign transaction request: $parameters');
final paramsData = parameters[0] as Map<String, dynamic>;
final message = _convertToReadable(paramsData);
final String? authError = await requestAuthorization(message);
if (authError != null) {
return authError;
}
// Load the private key
final List<ChainKeyModel> keys = wcKeyService
.getKeysForChain(appStore.wallet!);
final Credentials credentials = EthPrivateKey.fromHex(keys[0].privateKey);
WCEthereumTransactionModel ethTransaction =
WCEthereumTransactionModel.fromJson(parameters[0] as Map<String, dynamic>);
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<String> 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<ChainKeyModel> keys = wcKeyService
.getKeysForChain(appStore.wallet!);
return EthSigUtil.signTypedData(
privateKey: keys[0].privateKey,
jsonData: data ?? '',
version: TypedDataVersion.V4,
);
}
String _convertToReadable(Map<String, dynamic> 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
''';
}
}
@@ -1,28 +0,0 @@
class SolanaSignMessage {
final String pubkey;
final String message;
SolanaSignMessage({
required this.pubkey,
required this.message,
});
factory SolanaSignMessage.fromJson(Map<String, dynamic> json) {
return SolanaSignMessage(
pubkey: json['pubkey'] as String,
message: json['message'] as String,
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'pubkey': pubkey,
'message': message,
};
}
@override
String toString() {
return 'SolanaSignMessage(pubkey: $pubkey, message: $message)';
}
}
@@ -1,106 +0,0 @@
class SolanaSignTransaction {
final String? feePayer;
final String? recentBlockhash;
final String transaction;
final List<SolanaInstruction>? instructions;
SolanaSignTransaction({
required this.feePayer,
required this.recentBlockhash,
required this.instructions,
required this.transaction,
});
factory SolanaSignTransaction.fromJson(Map<String, dynamic> 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<dynamic>)
.map((e) => SolanaInstruction.fromJson(e as Map<String, dynamic>))
.toList(): null,
transaction: json['transaction'] as String,
);
}
Map<String, dynamic> 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<SolanaKeyMetadata> keys;
final List<int> data;
SolanaInstruction({
required this.programId,
required this.keys,
required this.data,
});
factory SolanaInstruction.fromJson(Map<String, dynamic> json) {
return SolanaInstruction(
programId: json['programId'] as String,
keys: (json['keys'] as List<dynamic>)
.map((e) => SolanaKeyMetadata.fromJson(e as Map<String, dynamic>))
.toList(),
data: (json['data'] as List<dynamic>).map((e) => e as int).toList(),
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'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<String, dynamic> json) {
return SolanaKeyMetadata(
pubkey: json['pubkey'] as String,
isSigner: json['isSigner'] as bool,
isWritable: json['isWritable'] as bool,
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'pubkey': pubkey,
'isSigner': isSigner,
'isWritable': isWritable,
};
}
@override
String toString() {
return 'SolanaKeyMetadata(pubkey: $pubkey, isSigner: $isSigner, isWritable: $isWritable)';
}
}
@@ -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';
}
}
@@ -1,170 +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 SolanaRPC solanaProvider;
final SolanaPrivateKey? ownerPrivateKey;
SolanaChainServiceImpl({
required this.reference,
required this.wcKeyService,
required this.bottomSheetService,
required this.wallet,
required this.ownerPrivateKey,
required String formattedRPCUrl,
SolanaRPC? solanaProvider,
}) : solanaProvider = solanaProvider ?? SolanaRPC(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<String> getEvents() {
return ['chainChanged', 'accountsChanged'];
}
Future<String?> 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<String> solanaSignTransaction(String topic, dynamic parameters) async {
log('received solana sign transaction request $parameters');
final solanaSignTx = SolanaSignTransaction.fromJson(parameters as Map<String, dynamic>);
final String? authError = await requestAuthorization('Confirm request to sign transaction?');
if (authError != null) {
return authError;
}
try {
// Convert transaction string to bytes
List<int> transactionBytes = base64Decode(solanaSignTx.transaction);
final message = SolanaTransactionUtils.deserializeMessageLegacy(transactionBytes);
final sign = ownerPrivateKey!.sign(message.serialize());
final signature = solanaProvider.request(
SolanaRPCSendTransaction(
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<String> solanaSignMessage(String topic, dynamic parameters) async {
log('received solana sign message request: $parameters');
final solanaSignMessage = SolanaSignMessage.fromJson(parameters as Map<String, dynamic>);
final String? authError = await requestAuthorization('Confirm request to sign message?');
if (authError != null) {
return authError;
}
List<int>? sign;
try {
sign = ownerPrivateKey!.sign(Base58Decoder.decode(solanaSignMessage.message));
} catch (e) {
printV(e);
}
if (sign == null) {
return '';
}
final signature = Base58Encoder.encode(sign);
return signature;
}
}
@@ -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<String, dynamic> 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<String, dynamic> 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)';
}
}
@@ -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)';
}
}
@@ -1,16 +0,0 @@
class ChainKeyModel {
final List<String> 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)';
}
}
@@ -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)';
}
}
@@ -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<PairingInfo> pairings;
@observable
ObservableList<SessionData> sessions;
@observable
ObservableList<StoredCacao> auth;
Web3WalletServiceBase(
this._bottomSheetHandler, this.walletKeyService, this.appStore, this.sharedPreferences)
: pairings = ObservableList<PairingInfo>(),
sessions = ObservableList<SessionData>(),
auth = ObservableList<StoredCacao>(),
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<ChainKeyModel> 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<void> 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<void> 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<void> _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<void> 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<void> _onSessionConnect(SessionConnect? args) async {
if (args != null) {
log('Session Connected $args');
await savePairingTopicToLocalStorage(args.session.pairingTopic);
sessions.add(args.session);
_refreshPairings();
}
}
@action
Future<void> _onAuthRequest(AuthRequest? args) async {
if (args != null) {
final chaindIdNamespace = getChainNameSpaceAndIdBasedOnWalletType(appStore.wallet!.type);
List<ChainKeyModel> 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<void> 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<SessionData> getSessionsForPairingInfo(PairingInfo pairing) {
return sessions.where((element) => element.pairingTopic == pairing.topic).toList();
}
String getKeyForStoringTopicsForWallet() {
List<ChainKeyModel> chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
if (chainKeys.isEmpty) {
return '';
}
final keyForPairingTopic =
PreferencesKey.walletConnectPairingTopicsListForWallet(chainKeys.first.publicKey);
return keyForPairingTopic;
}
List<String> 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<dynamic> jsonList = jsonDecode(jsonString) as List<dynamic>;
// Cast each item to a string
return jsonList.map((item) => item as String).toList();
}
Future<void> 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);
}
}
}
+14 -9
View File
@@ -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';
@@ -37,6 +34,9 @@ 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/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/link_view_model.dart';
import 'package:cake_wallet/tron/tron.dart';
@@ -636,11 +636,15 @@ Future<void> setup({
getIt.registerLazySingleton<WalletConnectKeyService>(() => KeyServiceImpl());
getIt.registerLazySingleton<Web3WalletService>(() {
final Web3WalletService web3WalletService = Web3WalletService(getIt.get<BottomSheetService>(),
getIt.get<WalletConnectKeyService>(), appStore, getIt.get<SharedPreferences>());
web3WalletService.create();
return web3WalletService;
getIt.registerLazySingleton<WalletKitService>(() {
final WalletKitService walletKitService = WalletKitService(
getIt.get<BottomSheetService>(),
getIt.get<WalletConnectKeyService>(),
appStore,
getIt.get<SharedPreferences>(),
);
walletKitService.create();
return walletKitService;
});
getIt.registerFactory(() => BalancePage(
@@ -1441,7 +1445,8 @@ Future<void> setup({
});
getIt.registerFactory(
() => WalletConnectConnectionsView(web3walletService: getIt.get<Web3WalletService>()));
() => WalletConnectConnectionsView(walletKitService: getIt.get<WalletKitService>()),
);
getIt.registerFactory(() => NFTViewModel(appStore, getIt.get<BottomSheetService>()));
getIt.registerFactory<TorPage>(() => TorPage(getIt.get<AppStore>()));
@@ -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<BottomSheetService>().resetCurrentSheet();
getIt.get<BottomSheetService>().showNext();
await navigatorKey.currentState!
.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false);
},
+17 -2
View File
@@ -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<String> 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:
+2 -2
View File
@@ -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';
@@ -106,6 +105,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';
@@ -777,7 +777,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.walletConnectConnectionsListing:
return MaterialPageRoute<void>(
builder: (_) => WalletConnectConnectionsView(
web3walletService: getIt.get<Web3WalletService>(),
walletKitService: getIt.get<WalletKitService>(),
launchUri: settings.arguments as Uri?,
));
@@ -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';
@@ -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';
@@ -6,11 +6,13 @@ class BottomSheetQueueItemModel {
final Widget widget;
final bool isModalDismissible;
final Completer<dynamic> completer;
final int closeAfter;
BottomSheetQueueItemModel({
required this.widget,
required this.completer,
this.isModalDismissible = false,
this.closeAfter = 0,
});
@override
@@ -1,10 +1,10 @@
class ConnectionModel {
class WCConnectionModel {
final String? title;
final String? text;
final List<String>? elements;
final Map<String, void Function()>? 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)';
}
}
@@ -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<BottomSheetQueueItemModel?> currentSheet;
Future<dynamic> queueBottomSheet({
required Widget widget,
bool isModalDismissible = false,
int closeAfter = 0,
});
void resetCurrentSheet();
void showNext();
}
class BottomSheetServiceImpl implements BottomSheetService {
Queue<BottomSheetQueueItemModel> queue = Queue<BottomSheetQueueItemModel>();
@override
final ValueNotifier<BottomSheetQueueItemModel?> currentSheet = ValueNotifier(null);
@@ -21,6 +26,7 @@ class BottomSheetServiceImpl implements BottomSheetService {
@override
Future<dynamic> 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();
}
}
}
@@ -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';
}
}
@@ -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<String, dynamic Function(String, dynamic)> get sessionRequestHandlers => {
EVMSupportedMethods.ethSign.name: ethSign,
EVMSupportedMethods.ethSignTransaction.name: ethSignTransaction,
EVMSupportedMethods.ethSignTypedData.name: ethSignTypedData,
EVMSupportedMethods.ethSignTypedDataV4.name: ethSignTypedDataV4,
};
Map<String, dynamic Function(String, dynamic)> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<dynamic> 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<dynamic> _approveTransaction(
Map<String, dynamic> 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<String> extractPermitData(dynamic data) async {
if (data is List && data.length >= 2) {
final typedData = jsonDecode(data[1] as String) as Map<String, dynamic>;
// Extracting domain details.
final domain = typedData['domain'] ?? {} as Map<String, dynamic>;
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<String, dynamic>;
final details = message['details'] ?? {} as Map<String, dynamic>;
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<String> 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<String, dynamic>;
final symbol = (decodedResponse['symbol'] ?? '') as String;
final name = decodedResponse['name'] ?? '';
return '$name ($symbol)';
}
}
@@ -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';
}
}
}
@@ -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';
}
}
@@ -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<String, dynamic Function(String, dynamic)> 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<void> 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<String, dynamic>;
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<void> 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<String, dynamic>;
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<dynamic>;
final instructions = instructionsList.map((json) {
return (json as Map<String, dynamic>).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<void> 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<String, dynamic>;
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<String>;
List<String> 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<dynamic> 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<String, dynamic> {
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<String, dynamic>);
return AccountMeta(
publicKey:
SolanaPublicKey.fromBytes(base58.decode(kParams['pubkey'] as String)).toAddress(),
isWritable: kParams['isWritable'] as bool,
isSigner: kParams['isSigner'] as bool,
);
}).toList(),
);
}
}
@@ -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';
}
}
}
@@ -0,0 +1,37 @@
import 'dart:convert';
class ChainKeyModel {
final List<String> 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<String, dynamic> toJson() => {
'chains': chains,
'privateKey': privateKey,
'publicKey': privateKey,
};
factory ChainKeyModel.fromJson(Map<String, dynamic> 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());
}
@@ -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';
@@ -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<PairingInfo>(),
sessions = ObservableList<SessionData>(),
auth = ObservableList<PendingSessionAuthRequest>(),
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<PairingInfo> pairings;
@observable
ObservableList<SessionData> sessions;
@observable
ObservableList<PendingSessionAuthRequest> 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<ChainKeyModel> 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<void> 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<void> _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<void> 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<void> _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<void> _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<void> _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<void> _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<Map<String, dynamic>> formattedMessages = [];
for (var chain in newAuthPayload.chains) {
final chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
final iss = 'did:pkh:$chain:${chainKeys.first.publicKey}';
final message = _walletKit.formatAuthMessage(
iss: iss,
cacaoPayload: cacaoRequestPayload,
);
formattedMessages.add({iss: message});
}
final WCBottomSheetResult result = (await _bottomSheetHandler.queueBottomSheet(
widget: WCSessionAuthRequestWidget(
child: WCConnectionRequestWidget(
sessionAuthPayload: newAuthPayload,
verifyContext: args.verifyContext,
requester: args.requester,
walletKeyService: walletKeyService,
walletKit: _walletKit,
appStore: appStore,
),
),
) as WCBottomSheetResult?) ??
WCBottomSheetResult.reject;
if (result != WCBottomSheetResult.reject) {
final chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
final privateKey = '0x${chainKeys.first.privateKey}';
final credentials = EthPrivateKey.fromHex(privateKey);
//
final messageToSign = formattedMessages.length;
final count = (result == WCBottomSheetResult.one) ? 1 : messageToSign;
//
final List<Cacao> cacaos = [];
for (var i = 0; i < count; i++) {
final iss = formattedMessages[i].keys.first;
final message = formattedMessages[i].values.first as String;
final signature = credentials.signPersonalMessageToUint8List(
Uint8List.fromList(message.codeUnits),
);
final hexSignature = bytesToHex(signature, include0x: true);
cacaos.add(
AuthSignature.buildAuthObject(
requestPayload: cacaoRequestPayload,
signature: CacaoSignature(t: CacaoSignature.EIP191, s: hexSignature),
iss: iss,
),
);
}
//
try {
final session = await _walletKit.approveSessionAuthenticate(
id: args.id,
auths: cacaos,
);
debugPrint('_onSessionAuthRequest - approveSessionAuthenticate $session');
MethodsUtils.handleRedirect(
session.topic,
session.session?.peer.metadata.redirect,
'',
true,
);
} on ReownSignError catch (error) {
MethodsUtils.handleRedirect(
args.topic,
args.requester.metadata.redirect,
error.message,
);
}
} else {
final error = Errors.getSdkError(Errors.USER_REJECTED_AUTH);
await _walletKit.rejectSessionAuthenticate(
id: args.id,
reason: error.toSignError(),
);
MethodsUtils.handleRedirect(
args.topic,
args.requester.metadata.redirect,
error.message,
);
}
}
}
@action
Future<void> deletePairing({required String topic}) async {
final topicSessions = sessions.where((element) => element.pairingTopic == topic);
await _walletKit.core.pairing.disconnect(topic: topic);
for (var session in topicSessions) {
await _walletKit.disconnectSession(
topic: session.topic,
reason: Errors.getSdkError(Errors.USER_DISCONNECTED).toSignError(),
);
}
}
@action
Future<void> disconnectSession({required String topic}) async {
await walletKit.disconnectSession(
topic: topic,
reason: Errors.getSdkError(Errors.USER_DISCONNECTED).toSignError(),
);
}
@action
Future<void> updateSession({
required String topic,
required Map<String, Namespace> namespaces,
}) async {
await walletKit.updateSession(topic: topic, namespaces: namespaces);
}
@action
Future<void> extendSession({required String topic}) async {
await walletKit.extendSession(topic: topic);
}
@action
Future<void> pairWithUri(Uri uri) async {
try {
debugPrint('pairWithUri - Pairing with URI: $uri');
await _walletKit.pair(uri: uri);
} on ReownSignError 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() {
debugPrint('_refreshPairings - Refreshing pairings');
pairings.clear();
final allPairings = _walletKit.pairings.getAll();
final keyForWallet = getKeyForStoringTopicsForWallet();
if (keyForWallet.isEmpty) return;
final currentTopicsForWallet = getPairingTopicsForWallet(keyForWallet);
final filteredPairings = allPairings.where(
(pairing) {
bool isInCurrentTopics = currentTopicsForWallet.contains(pairing.topic);
bool isActive = pairing.active;
return isInCurrentTopics && isActive;
},
).toList();
pairings.addAll(filteredPairings);
}
@action
List<SessionData> getSessionsForPairingInfo(PairingInfo pairing) {
return sessions.where((element) => element.pairingTopic == pairing.topic).toList();
}
String getKeyForStoringTopicsForWallet() {
List<ChainKeyModel> chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
if (chainKeys.isEmpty) {
return '';
}
final keyForPairingTopic =
PreferencesKey.walletConnectPairingTopicsListForWallet(chainKeys.first.publicKey);
return keyForPairingTopic;
}
List<String> 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<dynamic> jsonList = jsonDecode(jsonString) as List<dynamic>;
// Cast each item to a string
return jsonList.map((item) => item as String).toList();
}
Future<void> 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);
bool isPairingTopicAlreadySaved = pairingTopicsForWallet.contains(pairingTopic);
debugPrint('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);
}
}
}
@@ -0,0 +1,88 @@
import 'dart:convert';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
import 'package:convert/convert.dart';
import 'package:flutter/foundation.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
class EthUtils {
static String getUtf8Message(String maybeHex) {
if (maybeHex.startsWith('0x')) {
final List<int> decoded = hex.decode(
maybeHex.substring(2),
);
return utf8.decode(decoded);
}
return maybeHex;
}
static String? getAddressFromSessionRequest(SessionRequest request) {
try {
final paramsList = List<String?>.from((request.params as List));
if (request.method == 'personal_sign') {
// for `personal_sign` first value in params has to be always the message
paramsList.removeAt(0);
}
return paramsList.firstWhere((p) {
try {
EthereumAddress.fromHex(p ?? '');
return true;
} catch (e) {
return false;
}
});
} catch (e) {
debugPrint(e.toString());
return null;
}
}
static dynamic getDataFromSessionRequest(SessionRequest request) {
try {
final paramsList = List.from((request.params as List));
if (request.method == 'personal_sign') {
return paramsList.first;
}
return paramsList.firstWhere((p) {
final address = getAddressFromSessionRequest(request);
return p != address;
});
} catch (e) {
debugPrint('getDataFromSessionRequest $e');
return null;
}
}
static Map<String, dynamic>? getTransactionFromSessionRequest(
SessionRequest request,
) {
try {
final param = (request.params as List<dynamic>).first;
return param as Map<String, dynamic>;
} catch (e) {
debugPrint('getTransactionFromSessionRequest $e');
return null;
}
}
static Future<dynamic> decodeMessageEvent(MessageEvent event) async {
final walletKit = getIt<WalletKitService>().walletKit;
final payloadString = await walletKit.core.crypto.decode(
event.topic,
event.message,
);
if (payloadString == null) return null;
final data = jsonDecode(payloadString) as Map<String, dynamic>;
if (data.containsKey('method')) {
return JsonRpcRequest.fromJson(data);
} else {
return JsonRpcResponse.fromJson(data);
}
}
}
@@ -0,0 +1,139 @@
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/models/wc_connection_model.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_connection_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_request_widget.dart';
import 'package:flutter/material.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
class MethodsUtils {
static final walletKit = getIt.get<WalletKitService>().walletKit;
static final bottomSheetService = getIt.get<BottomSheetService>();
static Future<bool> requestApproval(
String text, {
String? title,
String? method,
String? chainId,
String? address,
required String transportType,
List<WCConnectionModel> extraModels = const [],
VerifyContext? verifyContext,
}) async {
final WCBottomSheetResult result = (await bottomSheetService.queueBottomSheet(
widget: WCRequestWidget(
verifyContext: verifyContext,
child: WCConnectionWidget(
title: title ?? S.current.approve_request,
info: [
WCConnectionModel(
title: '${S.current.method}: $method\n'
'${S.current.transport_type}: ${transportType.toUpperCase()}\n'
'${S.current.chain_id}: $chainId\n'
'${S.current.address}: $address\n\n'
'${S.current.message}:',
elements: [text],
),
...extraModels,
],
),
),
) as WCBottomSheetResult?) ??
WCBottomSheetResult.reject;
return result != WCBottomSheetResult.reject;
}
static void handleRedirect(
String topic,
Redirect? redirect, [
String? error,
bool success = false,
]) {
debugPrint('handleRedirect topic: $topic, redirect: $redirect, error: $error');
openApp(
topic,
redirect,
onFail: (e) => goBackModal(
title: success ? S.current.success : S.current.error,
message: error,
success: success,
),
);
}
static void openApp(
String topic,
Redirect? redirect, {
int delay = 100,
Function(ReownSignError? error)? onFail,
}) async {
await Future.delayed(Duration(milliseconds: delay));
try {
await walletKit.redirectToDapp(
topic: topic,
redirect: redirect,
);
} on ReownSignError catch (e) {
onFail?.call(e);
}
}
static void goBackModal({
String? title,
String? message,
bool success = true,
}) async {
await bottomSheetService.queueBottomSheet(
closeAfter: success ? 3 : 0,
widget: GoBackModalWidget(
isSuccess: success,
title: title,
message: message,
),
);
}
}
class GoBackModalWidget extends StatelessWidget {
const GoBackModalWidget({
required this.isSuccess,
this.message,
this.title,
super.key,
});
final bool isSuccess;
final String? title;
final String? message;
@override
Widget build(BuildContext context) {
return Container(
color: Theme.of(context).colorScheme.background,
height: 280.0,
width: double.infinity,
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
Icon(
isSuccess ? Icons.check_circle_sharp : Icons.error_outline_sharp,
color: isSuccess ? Colors.green[100] : Colors.red[100],
size: 80.0,
),
Text(
title ?? S.current.connected,
style: TextStyle(
color: Theme.of(context).appBarTheme.titleTextStyle!.color!,
fontSize: 18.0,
fontWeight: FontWeight.w600,
),
),
Text(message ?? S.current.youCanGoBackToYourDapp),
],
),
);
}
}
@@ -1,71 +1,76 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_widget.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
import '../../../../core/wallet_connect/models/connection_model.dart';
import 'package:cake_wallet/src/screens/wallet_connect/models/wc_connection_model.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_connection_widget.dart';
import 'package:flutter/material.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
class ConnectionWidgetBuilder {
static List<ConnectionWidget> buildFromRequiredNamespaces(
Map<String, RequiredNamespace> requiredNamespaces,
static List<WCConnectionWidget> buildFromRequiredNamespaces(
Map<String, Namespace> generatedNamespaces,
) {
final List<ConnectionWidget> views = [];
for (final key in requiredNamespaces.keys) {
RequiredNamespace ns = requiredNamespaces[key]!;
final List<ConnectionModel> models = [];
// If the chains property is present, add the chain data to the models
if (ns.chains != null) {
models.add(ConnectionModel(title: S.current.chains, elements: ns.chains!));
}
models.add(ConnectionModel(title: S.current.methods, elements: ns.methods));
models.add(ConnectionModel(title: S.current.events, elements: ns.events));
final List<WCConnectionWidget> views = [];
for (final key in generatedNamespaces.keys) {
final namespaces = generatedNamespaces[key]!;
final chains = NamespaceUtils.getChainsFromAccounts(namespaces.accounts);
views.add(ConnectionWidget(title: key, info: models));
final List<WCConnectionModel> models = [];
// If the chains property is present, add the chain data to the models
models.add(WCConnectionModel(title: S.current.chains, elements: chains));
models.add(WCConnectionModel(title: S.current.methods, elements: namespaces.methods));
if (namespaces.events.isNotEmpty) {
models.add(WCConnectionModel(title: S.current.events, elements: namespaces.events));
}
views.add(WCConnectionWidget(title: key, info: models));
}
return views;
}
static List<ConnectionWidget> buildFromNamespaces(
static List<WCConnectionWidget> buildFromNamespaces(
String topic,
Map<String, Namespace> namespaces,
Web3Wallet web3wallet,
BuildContext context,
) {
final List<ConnectionWidget> views = [];
final List<WCConnectionWidget> views = [];
for (final key in namespaces.keys) {
final Namespace ns = namespaces[key]!;
final List<ConnectionModel> models = [];
final ns = namespaces[key]!;
final List<WCConnectionModel> models = [];
// If the chains property is present, add the chain data to the models
models.add(
ConnectionModel(
title: S.current.chains,
elements: ns.accounts,
),
);
models.add(ConnectionModel(
title: S.current.methods,
elements: ns.methods,
));
models.add(WCConnectionModel(title: S.current.accounts, elements: ns.accounts));
models.add(WCConnectionModel(title: S.current.methods, elements: ns.methods));
Map<String, void Function()> actions = {};
for (final String event in ns.events) {
actions[event] = () async {
final String chainId = NamespaceUtils.isValidChainId(key)
? key
: NamespaceUtils.getChainFromAccount(ns.accounts.first);
await web3wallet.emitSessionEvent(
topic: topic,
chainId: chainId,
event: SessionEventParams(name: event, data: '${S.current.event}: $event'),
);
};
if (ns.events.isNotEmpty) {
models.add(WCConnectionModel(title: S.current.events, elements: ns.events));
}
models.add(
ConnectionModel(title: S.current.events, elements: ns.events, elementActions: actions),
);
views.add(ConnectionWidget(title: key, info: models));
views.add(WCConnectionWidget(title: key, info: models));
}
return views;
}
static Map<String, Namespace> updateNamespaces(
Map<String, Namespace> currentNamespaces,
String namespace,
List<String> newChains,
) {
final updatedNamespaces = Map<String, Namespace>.from(currentNamespaces);
final accounts = currentNamespaces[namespace]!.accounts;
final address = NamespaceUtils.getAccount(accounts.first);
final newAccounts = newChains.map((c) => '$c:$address').toList();
final newNamespaces = currentNamespaces[namespace]!.copyWith(
chains: NamespaceUtils.getChainsFromAccounts(accounts)..addAll(newChains),
accounts: List<String>.from(accounts)..addAll(newAccounts),
);
updatedNamespaces[namespace] = newNamespaces;
return updatedNamespaces;
}
}
@@ -1,20 +1,6 @@
import 'dart:convert';
import 'dart:math';
import 'package:convert/convert.dart';
extension StringParsing on String {
String get utf8Message {
if (startsWith('0x')) {
final List<int> decoded = hex.decode(
substring(2),
);
return utf8.decode(decoded);
}
return this;
}
String safeSubString(int start, int end) {
return this.substring(0, min(this.toString().length, 12));
}
@@ -1,7 +1,6 @@
import 'dart:developer';
import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/enter_wallet_connect_uri_widget.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
@@ -9,19 +8,19 @@ import 'package:cake_wallet/utils/device_info.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/utils/permission_handler.dart';
import 'widgets/pairing_item_widget.dart';
import 'widgets/wc_pairing_item_widget.dart';
import 'wc_pairing_detail_page.dart';
class WalletConnectConnectionsView extends StatelessWidget {
final Web3WalletService web3walletService;
final WalletKitService walletKitService;
WalletConnectConnectionsView({required this.web3walletService, Uri? launchUri, Key? key})
WalletConnectConnectionsView({required this.walletKitService, Uri? launchUri, Key? key})
: super(key: key) {
_triggerPairingFromDeeplink(launchUri);
}
@@ -39,42 +38,38 @@ class WalletConnectConnectionsView extends StatelessWidget {
final uriData = Uri.parse(uri);
await web3walletService.pairWithUri(uriData);
await walletKitService.pairWithUri(uriData);
}
@override
Widget build(BuildContext context) {
return WCPairingsWidget(web3walletService: web3walletService);
return WCPairingsWidget(walletKitService: walletKitService);
}
}
class WCPairingsWidget extends BasePage {
WCPairingsWidget({required this.web3walletService, Key? key})
: web3wallet = web3walletService.getWeb3Wallet();
WCPairingsWidget({required this.walletKitService, Key? key})
: walletKit = walletKitService.walletKit;
final Web3Wallet web3wallet;
final Web3WalletService web3walletService;
final ReownWalletKit walletKit;
final WalletKitService walletKitService;
@override
String get title => S.current.walletConnect;
Future<void> _onScanQrCode(BuildContext context, Web3Wallet web3Wallet) async {
Future<void> _onScanQrCode(BuildContext context, ReownWalletKit web3Wallet) async {
final String? uri;
if (DeviceInfo.instance.isMobile) {
bool isCameraPermissionGranted =
await PermissionHandler.checkPermission(Permission.camera, context);
await PermissionHandler.checkPermission(Permission.camera, context);
if (!isCameraPermissionGranted) return;
uri = await presentQRScanner(context);
} else {
uri = await _showEnterWalletConnectURIPopUp(context);
}
if (uri == null) return _invalidUriToast(context, S.current.nullURIError);
log('_onFoundUri: $uri');
final Uri uriData = Uri.parse(uri);
await web3walletService.pairWithUri(uriData);
await _handleWalletConnectURI(uri, context);
}
Future<String?> _showEnterWalletConnectURIPopUp(BuildContext context) async {
@@ -87,6 +82,17 @@ class WCPairingsWidget extends BasePage {
return walletConnectURI;
}
Future<void> _handleWalletConnectURI(
String? walletConnectURI,
BuildContext context,
) async {
if (walletConnectURI == null) return _invalidUriToast(context, S.current.nullURIError);
log('_onFoundUri: $walletConnectURI');
final Uri uriData = Uri.parse(walletConnectURI);
await walletKitService.pairWithUri(uriData);
}
Future<void> _invalidUriToast(BuildContext context, String message) async {
await showPopUp<void>(
context: context,
@@ -126,15 +132,30 @@ class WCPairingsWidget extends BasePage {
text: S.current.newConnection,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
onPressed: () => _onScanQrCode(context, web3wallet),
onPressed: () => _onScanQrCode(context, walletKit),
),
SizedBox(height: 4),
TextButton(
onPressed: () async {
final uri = await _showEnterWalletConnectURIPopUp(context);
await _handleWalletConnectURI(uri, context);
},
child: Text(
'Click to paste WalletConnect Link',
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
),
],
),
),
SizedBox(height: 48),
SizedBox(height: 16),
Expanded(
child: Visibility(
visible: web3walletService.pairings.isEmpty,
visible: walletKitService.pairings.isEmpty,
child: Center(
child: Text(
S.current.activeConnectionsPrompt,
@@ -147,10 +168,10 @@ class WCPairingsWidget extends BasePage {
),
),
replacement: ListView.builder(
itemCount: web3walletService.pairings.length,
itemCount: walletKitService.pairings.length,
itemBuilder: (BuildContext context, int index) {
final pairing = web3walletService.pairings[index];
return PairingItemWidget(
final pairing = walletKitService.pairings[index];
return WCPairingItemWidget(
key: ValueKey(pairing.topic),
pairing: pairing,
onTap: () {
@@ -159,7 +180,7 @@ class WCPairingsWidget extends BasePage {
MaterialPageRoute(
builder: (context) => WalletConnectPairingDetailsPage(
pairing: pairing,
web3walletService: web3walletService,
walletKitService: walletKitService,
),
),
);
@@ -1,24 +1,22 @@
import 'dart:developer';
import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:flutter/material.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
import 'utils/namespace_model_builder.dart';
class WalletConnectPairingDetailsPage extends StatefulWidget {
final PairingInfo pairing;
final Web3WalletService web3walletService;
final WalletKitService walletKitService;
const WalletConnectPairingDetailsPage({
required this.pairing,
required this.web3walletService,
required this.walletKitService,
super.key,
});
@@ -33,7 +31,9 @@ class WalletConnectPairingDetailsPageState extends State<WalletConnectPairingDet
void initState() {
super.initState();
initDateTime();
initSessions();
WidgetsBinding.instance.addPostFrameCallback((_) {
initSessions();
});
}
void initDateTime() {
@@ -46,13 +46,13 @@ class WalletConnectPairingDetailsPageState extends State<WalletConnectPairingDet
}
void initSessions() {
List<SessionData> sessions = widget.web3walletService.getSessionsForPairingInfo(widget.pairing);
List<SessionData> sessions = widget.walletKitService.getSessionsForPairingInfo(widget.pairing);
for (final SessionData session in sessions) {
List<Widget> namespaceWidget = ConnectionWidgetBuilder.buildFromNamespaces(
session.topic,
session.namespaces,
widget.web3walletService.getWeb3Wallet(),
context,
);
// Loop through and add the namespace widgets, but put 20 pixels between each one
for (int i = 0; i < namespaceWidget.length; i++) {
@@ -61,6 +61,59 @@ class WalletConnectPairingDetailsPageState extends State<WalletConnectPairingDet
sessionWidgets.add(const SizedBox(height: 20.0));
}
}
sessionWidgets.add(const SizedBox.square(dimension: 10.0));
sessionWidgets.add(
PrimaryButton(
onPressed: () async {
try {
await widget.walletKitService.extendSession(
topic: session.topic,
);
} catch (e) {
debugPrint('${e.toString()}');
}
},
text: S.current.extend_session,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
),
);
sessionWidgets.add(const SizedBox.square(dimension: 10.0));
sessionWidgets.add(
PrimaryButton(
onPressed: () async {
try {
await widget.walletKitService.updateSession(
topic: session.topic,
namespaces: session.namespaces,
);
} catch (e) {
debugPrint('${e.toString()}');
}
},
text: S.current.update_session,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
),
);
sessionWidgets.add(const SizedBox.square(dimension: 10.0));
sessionWidgets.add(
PrimaryButton(
onPressed: () async {
try {
await widget.walletKitService.disconnectSession(
topic: session.topic,
);
} catch (e) {
debugPrint('${e.toString()}');
}
},
text: S.current.disconnect_session,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
),
);
}
}
@@ -70,7 +123,7 @@ class WalletConnectPairingDetailsPageState extends State<WalletConnectPairingDet
widget.pairing,
expiryDate,
sessionWidgets,
widget.web3walletService,
widget.walletKitService,
);
}
}
@@ -80,13 +133,13 @@ class WCCDetailsWidget extends BasePage {
this.pairing,
this.expiryDate,
this.sessionWidgets,
this.web3walletService,
this.walletKitService,
);
final PairingInfo pairing;
final String expiryDate;
final List<Widget> sessionWidgets;
final Web3WalletService web3walletService;
final WalletKitService walletKitService;
@override
Widget body(BuildContext context) {
@@ -141,7 +194,7 @@ class WCCDetailsWidget extends BasePage {
const SizedBox(height: 20.0),
PrimaryButton(
onPressed: () =>
_onDeleteButtonPressed(context, pairing.peerMetadata!.name, web3walletService),
_onDeleteButtonPressed(context, pairing.peerMetadata!.name, walletKitService),
text: S.current.delete,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
@@ -154,7 +207,10 @@ class WCCDetailsWidget extends BasePage {
}
Future<void> _onDeleteButtonPressed(
BuildContext context, String dAppName, Web3WalletService web3walletService) async {
BuildContext context,
String dAppName,
WalletKitService walletKitService,
) async {
bool confirmed = false;
await showPopUp<void>(
@@ -175,11 +231,13 @@ class WCCDetailsWidget extends BasePage {
);
if (confirmed) {
try {
await web3walletService.disconnectSession(pairing.topic);
await walletKitService.deletePairing(topic: pairing.topic);
Navigator.of(context).pop();
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
} catch (e) {
log(e.toString());
debugPrint(e.toString());
}
}
}
@@ -0,0 +1,100 @@
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/models/bottom_sheet_queue_item_model.dart';
import 'package:flutter/material.dart';
class BottomSheetListener extends StatefulWidget {
final BottomSheetService bottomSheetService;
final Widget child;
const BottomSheetListener({
required this.bottomSheetService,
required this.child,
super.key,
});
@override
BottomSheetListenerState createState() => BottomSheetListenerState();
}
class BottomSheetListenerState extends State<BottomSheetListener> {
@override
void initState() {
super.initState();
widget.bottomSheetService.currentSheet.addListener(_showBottomSheet);
}
@override
void dispose() {
widget.bottomSheetService.currentSheet.removeListener(_showBottomSheet);
super.dispose();
}
Future<void> _showBottomSheet() async {
if (widget.bottomSheetService.currentSheet.value != null) {
BottomSheetQueueItemModel item = widget.bottomSheetService.currentSheet.value!;
final value = await showModalBottomSheet(
context: context,
isDismissible: item.isModalDismissible,
backgroundColor: Color.fromARGB(0, 0, 0, 0),
isScrollControlled: true,
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.9),
builder: (context) {
if (item.closeAfter > 0) {
Future.delayed(Duration(seconds: item.closeAfter), () {
try {
if (!mounted) return;
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
} catch (e, s) {
debugPrint('[$runtimeType] close $e $s');
}
});
}
return Material(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.all(Radius.circular(16)),
child: Padding(
padding: EdgeInsets.only(
top: 16,
left: 16,
right: 16,
bottom: MediaQuery.of(context).viewInsets.bottom + 24,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
color: Theme.of(context).appBarTheme.titleTextStyle!.color!,
padding: const EdgeInsets.all(0.0),
visualDensity: VisualDensity.compact,
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
},
icon: const Icon(Icons.close_sharp),
),
],
),
Flexible(child: item.widget),
],
),
),
);
},
);
if (!item.completer.isCompleted) {
item.completer.complete(value);
}
widget.bottomSheetService.showNext();
}
}
@override
Widget build(BuildContext context) => widget.child;
}
@@ -1,4 +1,5 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:flutter/material.dart';
class BottomSheetMessageDisplayWidget extends StatelessWidget {
@@ -18,7 +19,7 @@ class BottomSheetMessageDisplayWidget extends StatelessWidget {
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
color: Colors.white,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
SizedBox(height: 8),
@@ -30,7 +31,7 @@ class BottomSheetMessageDisplayWidget extends StatelessWidget {
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Colors.white,
color: Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor,
),
),
),
@@ -1,180 +0,0 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter/material.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import '../../../../core/wallet_connect/models/auth_request_model.dart';
import '../../../../core/wallet_connect/models/connection_model.dart';
import '../../../../core/wallet_connect/models/session_request_model.dart';
import '../utils/namespace_model_builder.dart';
import 'connection_widget.dart';
class ConnectionRequestWidget extends StatefulWidget {
const ConnectionRequestWidget({
required this.wallet,
required this.chaindIdNamespace,
this.authRequest,
this.sessionProposal,
Key? key,
}) : super(key: key);
final Web3Wallet wallet;
final String chaindIdNamespace;
final AuthRequestModel? authRequest;
final SessionRequestModel? sessionProposal;
@override
State<ConnectionRequestWidget> createState() => _ConnectionRequestWidgetState();
}
class _ConnectionRequestWidgetState extends State<ConnectionRequestWidget> {
ConnectionMetadata? metadata;
@override
void initState() {
super.initState();
// Get the connection metadata
metadata = widget.authRequest?.request.requester ?? widget.sessionProposal?.request.proposer;
}
@override
Widget build(BuildContext context) {
if (metadata == null) {
return Text(
S.current.error,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor,
),
);
}
return _ConnectionMetadataDisplayWidget(
metadata: metadata,
wallet: widget.wallet,
authRequest: widget.authRequest,
sessionProposal: widget.sessionProposal,
chaindIdNamespace: widget.chaindIdNamespace,
);
}
}
class _ConnectionMetadataDisplayWidget extends StatelessWidget {
const _ConnectionMetadataDisplayWidget({
required this.wallet,
required this.metadata,
required this.sessionProposal,
required this.chaindIdNamespace,
this.authRequest,
});
final ConnectionMetadata? metadata;
final Web3Wallet wallet;
final String chaindIdNamespace;
final AuthRequestModel? authRequest;
final SessionRequestModel? sessionProposal;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Color.fromARGB(255, 18, 18, 19),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
metadata!.metadata.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor,
),
textAlign: TextAlign.center,
),
Text(
S.current.wouoldLikeToConnect,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
metadata!.metadata.url,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Visibility(
visible: authRequest != null,
child: _AuthRequestWidget(
wallet: wallet,
authRequest: authRequest,
chaindIdNamespace: chaindIdNamespace,
),
//If authRequest is null, sessionProposal is not null.
replacement: _SessionProposalWidget(sessionProposal: sessionProposal!),
),
],
),
);
}
}
class _AuthRequestWidget extends StatelessWidget {
const _AuthRequestWidget({
required this.wallet,
required this.chaindIdNamespace,
this.authRequest,
});
final Web3Wallet wallet;
final String chaindIdNamespace;
final AuthRequestModel? authRequest;
@override
Widget build(BuildContext context) {
final model = ConnectionModel(
text: wallet.formatAuthMessage(
iss: 'did:pkh:$chaindIdNamespace:${authRequest!.iss}',
cacaoPayload: CacaoRequestPayload.fromPayloadParams(
authRequest!.request.payloadParams,
),
),
);
return ConnectionWidget(
title: S.current.message,
info: [model],
);
}
}
class _SessionProposalWidget extends StatelessWidget {
const _SessionProposalWidget({required this.sessionProposal});
final SessionRequestModel sessionProposal;
@override
Widget build(BuildContext context) {
// Create the connection models using the required and optional namespaces provided by the proposal data
// The key is the title and the list of values is the data
final List<ConnectionWidget> views = ConnectionWidgetBuilder.buildFromRequiredNamespaces(
sessionProposal.request.requiredNamespaces,
);
return Column(children: views);
}
}
@@ -1,5 +1,6 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/base_alert_dialog.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -29,14 +30,14 @@ class _EnterWallectConnectURIWrapperWidgetState extends State<EnterWalletConnect
@override
Widget build(BuildContext context) {
return _EnterWalletConnectURIWidget(
return EnterWalletConnectURIWidget(
controller: controller,
);
}
}
class _EnterWalletConnectURIWidget extends BaseAlertDialog {
_EnterWalletConnectURIWidget({
class EnterWalletConnectURIWidget extends BaseAlertDialog {
EnterWalletConnectURIWidget({
required this.controller,
});
@@ -57,18 +58,25 @@ class _EnterWalletConnectURIWidget extends BaseAlertDialog {
@override
Widget content(BuildContext context) {
return Card(
color: Theme.of(context).dialogBackgroundColor,
elevation: 0.0,
margin: EdgeInsets.zero,
child: Column(
children: [
SizedBox(height: 8),
Text(
S.current.copyWalletConnectLink,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall,
),
SizedBox(height: 16),
TextField(
controller: controller,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor,
),
decoration: InputDecoration(
suffixIcon: Container(
width: 24,
@@ -79,14 +87,13 @@ class _EnterWalletConnectURIWidget extends BaseAlertDialog {
child: InkWell(
onTap: () => _pasteWalletConnectURI(),
child: Container(
padding: EdgeInsets.all(8),
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(6)),
),
child: Image.asset(
'assets/images/paste_ios.png',
color:
Theme.of(context).extension<SendPageTheme>()!.textFieldButtonIconColor,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
),
@@ -99,7 +106,7 @@ class _EnterWalletConnectURIWidget extends BaseAlertDialog {
),
),
hintStyle: TextStyle(
color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor,
color: Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor,
fontWeight: FontWeight.w500,
fontSize: 14,
),
@@ -1,64 +0,0 @@
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:flutter/material.dart';
import '../../../../../core/wallet_connect/models/bottom_sheet_queue_item_model.dart';
class BottomSheetListener extends StatefulWidget {
final BottomSheetService bottomSheetService;
final Widget child;
const BottomSheetListener({
required this.child,
required this.bottomSheetService,
super.key,
});
@override
BottomSheetListenerState createState() => BottomSheetListenerState();
}
class BottomSheetListenerState extends State<BottomSheetListener> {
@override
void initState() {
super.initState();
widget.bottomSheetService.currentSheet.addListener(_showBottomSheet);
}
@override
void dispose() {
widget.bottomSheetService.currentSheet.removeListener(_showBottomSheet);
super.dispose();
}
Future<void> _showBottomSheet() async {
if (widget.bottomSheetService.currentSheet.value != null) {
BottomSheetQueueItemModel item = widget.bottomSheetService.currentSheet.value!;
final value = await showModalBottomSheet(
context: context,
isDismissible: item.isModalDismissible,
backgroundColor: Color.fromARGB(0, 0, 0, 0),
isScrollControlled: true,
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.9),
builder: (context) {
return Container(
decoration: const BoxDecoration(
color: Color.fromARGB(255, 18, 18, 19),
borderRadius: BorderRadius.all(Radius.circular(16)),
),
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.all(16),
child: item.widget,
);
},
);
if (!item.completer.isCompleted) {
item.completer.complete(value);
}
widget.bottomSheetService.resetCurrentSheet();
}
}
@override
Widget build(BuildContext context) => widget.child;
}
@@ -1,48 +0,0 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:flutter/material.dart';
class Web3RequestModal extends StatelessWidget {
const Web3RequestModal({required this.child, this.onAccept, this.onReject, super.key});
final Widget child;
final VoidCallback? onAccept;
final VoidCallback? onReject;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
child,
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: PrimaryButton(
onPressed: onReject ?? () => Navigator.of(context).pop(false),
text: S.current.reject,
color: Theme.of(context).colorScheme.error,
textColor: Theme.of(context).colorScheme.onError,
),
),
const SizedBox(width: 16),
Expanded(
child: PrimaryButton(
onPressed: onAccept ?? () => Navigator.of(context).pop(true),
text: S.current.approve,
color: Theme.of(context).primaryColor,
textColor: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
],
),
],
),
);
}
}
@@ -1,11 +1,11 @@
import 'package:cake_wallet/src/screens/wallet_connect/models/wc_connection_model.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:flutter/material.dart';
import '../../../../core/wallet_connect/models/connection_model.dart';
class ConnectionItemWidget extends StatelessWidget {
const ConnectionItemWidget({required this.model, Key? key}) : super(key: key);
class WCConnectionItemWidget extends StatelessWidget {
const WCConnectionItemWidget({required this.model, Key? key}) : super(key: key);
final ConnectionModel model;
final WCConnectionModel model;
@override
Widget build(BuildContext context) {
@@ -52,7 +52,7 @@ class ConnectionItemWidget extends StatelessWidget {
class _NoModelElementWidget extends StatelessWidget {
const _NoModelElementWidget({required this.model});
final ConnectionModel model;
final WCConnectionModel model;
@override
Widget build(BuildContext context) {
@@ -73,7 +73,7 @@ class _ModelElementWidget extends StatelessWidget {
required this.modelElement,
});
final ConnectionModel model;
final WCConnectionModel model;
final String modelElement;
@override
@@ -0,0 +1,100 @@
import 'package:cake_wallet/generated/i18n.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/namespace_model_builder.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_connection_widget.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:flutter/material.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
class WCConnectionRequestWidget extends StatelessWidget {
WCConnectionRequestWidget({
this.sessionAuthPayload,
this.proposalData,
this.requester,
this.verifyContext,
required this.walletKeyService,
required this.walletKit,
required this.appStore,
});
final SessionAuthPayload? sessionAuthPayload;
final ProposalData? proposalData;
final ConnectionMetadata? requester;
final VerifyContext? verifyContext;
final WalletConnectKeyService walletKeyService;
final AppStore appStore;
final ReownWalletKit walletKit;
@override
Widget build(BuildContext context) {
if (requester == null) {
return Text(S.current.error.toUpperCase());
}
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
Text(
'${requester!.metadata.name} ${S.current.wouoldLikeToConnect}',
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
(sessionAuthPayload != null)
? _buildSessionAuthRequestView()
: _buildSessionProposalView(context),
],
),
);
}
Widget _buildSessionAuthRequestView() {
final cacaoPayload = CacaoRequestPayload.fromSessionAuthPayload(
sessionAuthPayload!,
);
final List<WCConnectionModel> messagesModels = [];
for (var chain in sessionAuthPayload!.chains) {
final chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
final iss = 'did:pkh:$chain:${chainKeys.first.publicKey}';
final message = walletKit.formatAuthMessage(
iss: iss,
cacaoPayload: cacaoPayload,
);
messagesModels.add(
WCConnectionModel(
title: '${S.current.message} ${messagesModels.length + 1}',
elements: [message],
),
);
}
return WCConnectionWidget(
title: '${messagesModels.length} ${S.current.messages}',
info: messagesModels,
);
}
Widget _buildSessionProposalView(BuildContext context) {
// Create the connection models using the required and optional namespaces provided by the proposal data
final views = ConnectionWidgetBuilder.buildFromRequiredNamespaces(
proposalData!.generatedNamespaces ?? {},
);
return Column(children: views);
}
}
@@ -1,17 +1,17 @@
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/src/screens/wallet_connect/models/wc_connection_model.dart';
import 'package:flutter/material.dart';
import '../../../../core/wallet_connect/models/connection_model.dart';
import 'connection_item_widget.dart';
import 'wc_connection_item_widget.dart';
class ConnectionWidget extends StatelessWidget {
const ConnectionWidget({required this.title, required this.info, super.key});
class WCConnectionWidget extends StatelessWidget {
const WCConnectionWidget({required this.title, required this.info, super.key});
final String title;
final List<ConnectionModel> info;
final List<WCConnectionModel> info;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).primaryColorLight,
@@ -23,7 +23,7 @@ class ConnectionWidget extends StatelessWidget {
children: [
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
color: Theme.of(context) .colorScheme.background,
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 8),
@@ -32,12 +32,12 @@ class ConnectionWidget extends StatelessWidget {
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
color: Theme.of(context).appBarTheme.titleTextStyle!.color!
),
),
),
const SizedBox(height: 8),
...info.map((e) => ConnectionItemWidget(model: e)),
...info.map((e) => WCConnectionItemWidget(model: e)),
],
),
);
@@ -3,10 +3,10 @@ import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/receive_page_theme.dart';
import 'package:flutter/material.dart';
import 'package:walletconnect_flutter_v2/apis/core/pairing/utils/pairing_models.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
class PairingItemWidget extends StatelessWidget {
const PairingItemWidget({required this.pairing, required this.onTap, super.key});
class WCPairingItemWidget extends StatelessWidget {
const WCPairingItemWidget({required this.pairing, required this.onTap, super.key});
final PairingInfo pairing;
final void Function() onTap;
@@ -14,6 +14,7 @@ class PairingItemWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
PairingMetadata? metadata = pairing.peerMetadata;
if (metadata == null) {
return SizedBox.shrink();
}
@@ -0,0 +1,79 @@
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_verify_context_widget.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:flutter/material.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
class WCRequestWidget extends StatelessWidget {
WCRequestWidget({
required this.child,
this.verifyContext,
this.onAccept,
this.onReject,
});
final Widget child;
final VerifyContext? verifyContext;
final VoidCallback? onAccept;
final VoidCallback? onReject;
@override
Widget build(BuildContext context) {
final currentTheme = getIt.get<SettingsStore>().currentTheme;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
WCVerifyContextWidget(
currentTheme: currentTheme,
verifyContext: verifyContext,
),
const SizedBox(height: 8),
Flexible(
child: SingleChildScrollView(child: child),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: PrimaryButton(
onPressed: onReject ??
() {
if (Navigator.canPop(context)) {
Navigator.of(context).pop(WCBottomSheetResult.reject);
}
},
text: S.current.reject,
color: Theme.of(context).colorScheme.error,
textColor: Theme.of(context).colorScheme.onError,
),
),
const SizedBox(width: 16),
Expanded(
child: PrimaryButton(
onPressed: onAccept ??
() {
if (Navigator.canPop(context)) {
Navigator.of(context).pop(WCBottomSheetResult.one);
}
},
text: S.current.approve,
color: Theme.of(context).primaryColor,
textColor: currentTheme.type == ThemeType.dark
? Theme.of(context).extension<DashboardPageTheme>()!.textColor
: Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor,
),
),
],
),
],
);
}
}
@@ -0,0 +1,61 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:flutter/material.dart';
class WCSessionAuthRequestWidget extends StatelessWidget {
const WCSessionAuthRequestWidget({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: SingleChildScrollView(child: child),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
PrimaryButton(
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.of(context).pop(WCBottomSheetResult.reject);
}
},
text: S.current.cancel,
color: Theme.of(context).colorScheme.error,
textColor: Theme.of(context).colorScheme.onError,
),
const SizedBox(width: 8),
PrimaryButton(
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.of(context).pop(WCBottomSheetResult.one);
}
},
text: S.current.sign_one,
color: Theme.of(context).primaryColor,
textColor: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
const SizedBox(width: 8),
PrimaryButton(
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.of(context).pop(WCBottomSheetResult.all);
}
},
text: S.current.sign_all,
color: Theme.of(context).secondaryHeaderColor,
textColor: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
],
),
],
);
}
}
@@ -0,0 +1,137 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:flutter/material.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
class WCVerifyContextWidget extends StatelessWidget {
const WCVerifyContextWidget({
super.key,
required this.verifyContext,
required this.currentTheme,
});
final VerifyContext? verifyContext;
final ThemeBase currentTheme;
@override
Widget build(BuildContext context) {
if (verifyContext == null) {
return const SizedBox.shrink();
}
if (verifyContext!.validation.scam) {
return VerifyBanner(
color: Theme.of(context).colorScheme.error,
origin: verifyContext!.origin,
title: S.current.security_risk,
text: S.current.security_risk_description,
);
}
if (verifyContext!.validation.invalid) {
return VerifyBanner(
color: Theme.of(context).colorScheme.error,
origin: verifyContext!.origin,
title: S.current.domain_mismatch,
text: S.current.domain_mismatch_description,
);
}
if (verifyContext!.validation.valid) {
return VerifyHeader(
iconColor: currentTheme.type == ThemeType.dark
? Theme.of(context).extension<DashboardPageTheme>()!.textColor
: Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor,
title: verifyContext!.origin,
);
}
return VerifyBanner(
color: Colors.orange,
origin: verifyContext!.origin,
title: S.current.cannot_verify,
text: S.current.cannot_verify_description,
);
}
}
class VerifyHeader extends StatelessWidget {
const VerifyHeader({
super.key,
required this.iconColor,
required this.title,
});
final Color iconColor;
final String title;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.shield_outlined,
color: iconColor,
),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
color: iconColor,
fontWeight: FontWeight.bold,
),
),
],
);
}
}
class VerifyBanner extends StatelessWidget {
const VerifyBanner({
super.key,
required this.origin,
required this.title,
required this.text,
required this.color,
});
final String origin, title, text;
final Color color;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
origin,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
const SizedBox.square(dimension: 8.0),
Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: color.withOpacity(0.2),
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
),
child: Column(
children: [
VerifyHeader(
iconColor: color,
title: title,
),
const SizedBox(height: 4.0),
Text(
text,
textAlign: TextAlign.center,
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
);
}
}
+5 -4
View File
@@ -1,7 +1,8 @@
import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
import 'package:cake_wallet/utils/exception_handler.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/wallet_type.dart';
@@ -46,9 +47,9 @@ abstract class AppStoreBase with Store {
this.wallet!.setExceptionHandler(ExceptionHandler.onError);
if (isWalletConnectCompatibleChain(wallet.type)) {
await getIt.get<Web3WalletService>().onDispose();
getIt.get<Web3WalletService>().create();
await getIt.get<Web3WalletService>().init();
await getIt.get<WalletKitService>().onDispose();
getIt.get<WalletKitService>().create();
await getIt.get<WalletKitService>().init();
}
getIt.get<SharedPreferences>().setString(PreferencesKey.currentWalletName, wallet.name);
getIt
+4 -3
View File
@@ -1,10 +1,11 @@
import 'dart:convert';
import 'dart:developer';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/entities/solana_nft_asset_model.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/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_message_display_widget.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:http/http.dart' as http;
import 'package:mobx/mobx.dart';
@@ -121,7 +122,7 @@ abstract class NFTViewModelBase with Store {
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: e.toString(),
message: S.current.moralis_nft_error,
),
);
}