Cw 451 wallet connect for ethereum (#1049)

* Update Flutter
Update packages

* Feat: Wallet connect for ethereum

* Fix localization issues
Fix UI issues
Update old packages
Update workflow
Update how to build guide

* feat: Wallet connect

* feat: Add wallet connect for ethereum

* chore: Add eth dependencies in configure file

* Minor: `WalletConnect` settings name, not `Wallet connect`

* fix: Merge conflicts

* fix: Issues with test cases on various dApps, introduce Arbitrum rinkerby as suported chain

* ui: Design fixes for WalletConnect flow

* chore: Update repo and comment out send apk to channel in workflow

* fix: Core implementation

* feat: WalletConnect WIP

* feat: WalletConnect WIP

* feat: WalletConnect WIP

* chore: Unused parameters WIP [skip ci]

* fix: Code review fixes

* Feat: WalletConnect feat WIP

* feat: WalletConnect

* feat: WalletConnect

* feat: WalletConnect

* Feat: WalletConnect

* Feat: WalletConnect

* feat: Remove queue support for the bottomsheet

* feat: WalletConnect feature, bug fixes, folder restructuring, localization

* Feat: Add positive feedback prompt on successful transaction

* fix: Delete session bug

* fix: dependencies registration WIP

* feat: Registering dependencies for walletconnect

* chore: Move key data to secrets

* chore: ensure appropriate null checks

* chore: localization

* chore: Remove unused code

* localization

* chore: Remove unused code

* chore: Remove unused code

* chore: Add walletconnect project id key entry

* fix: Revert bash command for linnux support

* fix: Issues with translation in some languages and making unneeded external variable private

* fix: Add bottomsheet listener to desktop dashboard page

* Generalize ethereum not enough gas error check

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
Co-authored-by: Justin Ehrenhofer <justin.ehrenhofer@gmail.com>
This commit is contained in:
Adegoke David
2023-10-03 17:56:10 +03:00
committed by GitHub
co-authored by OmarHatem Justin Ehrenhofer
parent ae71f2045b
commit 32643823e5
67 changed files with 2819 additions and 314 deletions
@@ -0,0 +1,5 @@
abstract class ChainService {
String getNamespace();
String getChainId();
List<String> getEvents();
}
@@ -0,0 +1,60 @@
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)';
}
}
+35
View File
@@ -0,0 +1,35 @@
import 'package:cake_wallet/core/wallet_connect/evm_chain_service.dart';
enum EVMChainId {
ethereum,
polygon,
goerli,
mumbai,
arbitrum,
}
extension EVMChainIdX on EVMChainId {
String chain() {
String name = '';
switch (this) {
case EVMChainId.ethereum:
name = '1';
break;
case EVMChainId.polygon:
name = '137';
break;
case EVMChainId.goerli:
name = '5';
break;
case EVMChainId.arbitrum:
name = '42161';
break;
case EVMChainId.mumbai:
name = '80001';
break;
}
return '${EvmChainServiceImpl.namespace}:$name';
}
}
@@ -0,0 +1,294 @@
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/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/src/screens/wallet_connect/widgets/error_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:cw_core/wallet_type.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? ethClient,
}) : ethClient = ethClient ??
Web3Client(
appStore.settingsStore.getCurrentNode(WalletType.ethereum).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(getChainId());
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(getChainId());
final EthPrivateKey credentials = EthPrivateKey.fromHex(keys[0].privateKey);
final String signature = hex.encode(
credentials.signPersonalMessageToUint8List(
Uint8List.fromList(utf8.encode(message)),
),
);
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(getChainId());
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);
log('Result: $result');
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: S.current.awaitDAppProcessing,
isError: false,
),
);
return result;
} catch (e) {
log('An error has occured 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(getChainId());
return EthSigUtil.signTypedData(
privateKey: keys[0].privateKey,
jsonData: data ?? '',
version: TypedDataVersion.V4,
);
}
String _convertToReadable(Map<String, dynamic> data) {
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() + ' ETH'
: '0 ETH';
String from = data['from'] as String;
String to = data['to'] as String;
return '''
Gas: $gas\n
Value: $value\n
From: $from\n
To: $to
''';
}
}
@@ -0,0 +1,16 @@
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)';
}
}
@@ -0,0 +1,20 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
class BottomSheetQueueItemModel {
final Widget widget;
final bool isModalDismissible;
final Completer<dynamic> completer;
BottomSheetQueueItemModel({
required this.widget,
required this.completer,
this.isModalDismissible = false,
});
@override
String toString() {
return 'BottomSheetQueueItemModel(widget: $widget, completer: $completer)';
}
}
@@ -0,0 +1,16 @@
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)';
}
}
@@ -0,0 +1,18 @@
class ConnectionModel {
final String? title;
final String? text;
final List<String>? elements;
final Map<String, void Function()>? elementActions;
ConnectionModel({
this.title,
this.text,
this.elements,
this.elementActions,
});
@override
String toString() {
return 'WalletConnectRequestModel(title: $title, text: $text, elements: $elements, elementActions: $elementActions)';
}
}
@@ -0,0 +1,14 @@
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)';
}
}
@@ -0,0 +1,72 @@
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart';
import 'package:cw_core/balance.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/wallet_base.dart';
abstract class WalletConnectKeyService {
/// Returns a list of all the keys.
List<ChainKeyModel> getKeys();
/// Returns a list of all the chain ids.
List<String> getChains();
/// Returns a list of all the keys for a given chain id.
/// If the chain is not found, returns an empty list.
/// - [chain]: The chain to get the keys for.
List<ChainKeyModel> getKeysForChain(String chain);
/// Returns a list of all the accounts in namespace:chainId:address format.
List<String> getAllAccounts();
}
class KeyServiceImpl implements WalletConnectKeyService {
KeyServiceImpl(this.wallet)
: _keys = [
ChainKeyModel(
chains: [
'eip155:1',
'eip155:5',
'eip155:137',
'eip155:42161',
'eip155:80001',
],
privateKey: ethereum!.getPrivateKey(wallet),
publicKey: ethereum!.getPublicKey(wallet),
),
];
late final WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
late final List<ChainKeyModel> _keys;
@override
List<String> getChains() {
final List<String> chainIds = [];
for (final ChainKeyModel key in _keys) {
chainIds.addAll(key.chains);
}
return chainIds;
}
@override
List<ChainKeyModel> getKeys() => _keys;
@override
List<ChainKeyModel> getKeysForChain(String chain) {
return _keys.where((e) => e.chains.contains(chain)).toList();
}
@override
List<String> getAllAccounts() {
final List<String> accounts = [];
for (final ChainKeyModel key in _keys) {
for (final String chain in key.chains) {
accounts.add('$chain:${key.publicKey}');
}
}
return accounts;
}
}
@@ -0,0 +1,43 @@
import 'dart:async';
import 'package:cake_wallet/core/wallet_connect/models/bottom_sheet_queue_item_model.dart';
import 'package:flutter/material.dart';
abstract class BottomSheetService {
abstract final ValueNotifier<BottomSheetQueueItemModel?> currentSheet;
Future<dynamic> queueBottomSheet({
required Widget widget,
bool isModalDismissible = false,
});
void resetCurrentSheet();
}
class BottomSheetServiceImpl implements BottomSheetService {
@override
final ValueNotifier<BottomSheetQueueItemModel?> currentSheet = ValueNotifier(null);
@override
Future<dynamic> queueBottomSheet({
required Widget widget,
bool isModalDismissible = false,
}) async {
// Create the bottom sheet queue item
final completer = Completer<dynamic>();
final queueItem = BottomSheetQueueItemModel(
widget: widget,
completer: completer,
isModalDismissible: isModalDismissible,
);
currentSheet.value = queueItem;
return await completer.future;
}
@override
void resetCurrentSheet() {
currentSheet.value = null;
}
}
@@ -0,0 +1,277 @@
import 'dart:async';
import 'dart:developer';
import 'dart:typed_data';
import 'package:cake_wallet/core/wallet_connect/evm_chain_id.dart';
import 'package:cake_wallet/core/wallet_connect/evm_chain_service.dart';
import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.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/src/screens/wallet_connect/widgets/connection_request_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/error_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:eth_sig_util/eth_sig_util.dart';
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.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 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)
: 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();
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);
for (final cId in EVMChainId.values) {
EvmChainServiceImpl(
reference: cId,
appStore: appStore,
wcKeyService: walletKeyService,
bottomSheetService: _bottomSheetHandler,
wallet: _web3Wallet,
);
}
}
@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);
}
Web3Wallet getWeb3Wallet() {
return _web3Wallet;
}
void _onPairingsSync(StoreSyncEvent? args) {
if (args != null) {
_refreshPairings();
}
}
void _onPairingDelete(PairingEvent? event) {
_refreshPairings();
}
@action
void _refreshPairings() {
pairings.clear();
final allPairings = _web3Wallet.pairings.getAll();
pairings.addAll(allPairings);
}
Future<void> _onSessionProposalError(SessionProposalErrorEvent? args) async {
log(args.toString());
}
void _onSessionProposal(SessionProposalEvent? args) async {
if (args != null) {
final Widget modalWidget = Web3RequestModal(
child: ConnectionRequestWidget(
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'),
);
}
void _onPairingCreate(PairingEvent? args) {
log('Pairing Create Event: $args');
}
@action
void _onSessionConnect(SessionConnect? args) {
if (args != null) {
sessions.add(args.session);
}
}
@action
Future<void> _onAuthRequest(AuthRequest? args) async {
if (args != null) {
List<ChainKeyModel> chainKeys = walletKeyService.getKeysForChain('eip155:1');
// Create the message to be signed
final String iss = 'did:pkh:eip155:1:${chainKeys.first.publicKey}';
final Widget modalWidget = Web3RequestModal(
child: ConnectionRequestWidget(
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();
}
}
+34 -5
View File
@@ -3,10 +3,12 @@ import 'package:cake_wallet/anonpay/anonpay_info_base.dart';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
import 'package:cake_wallet/buy/payfura/payfura_buy_provider.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/buy/robinhood/robinhood_buy_provider.dart';
import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart';
import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/entities/background_tasks.dart';
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/receive_page_option.dart';
@@ -399,6 +401,10 @@ Future<void> setup({
}
if (appStore.wallet != null) {
authStore.allowed();
if (appStore.wallet!.type == WalletType.ethereum) {
getIt.get<Web3WalletService>().init();
}
return;
}
@@ -419,6 +425,10 @@ Future<void> setup({
} else {
if (appStore.wallet != null) {
authStore.allowed();
if (appStore.wallet!.type == WalletType.ethereum) {
getIt.get<Web3WalletService>().init();
}
return;
}
@@ -438,11 +448,28 @@ Future<void> setup({
}, closable: false);
}, instanceName: 'login');
getIt.registerSingleton<BottomSheetService>(BottomSheetServiceImpl());
final appStore = getIt.get<AppStore>();
getIt.registerLazySingleton<WalletConnectKeyService>(() => KeyServiceImpl(appStore.wallet!));
getIt.registerLazySingleton<Web3WalletService>(() {
final Web3WalletService web3WalletService = Web3WalletService(
getIt.get<BottomSheetService>(),
getIt.get<WalletConnectKeyService>(),
appStore,
);
web3WalletService.create();
return web3WalletService;
});
getIt.registerFactory(() => BalancePage(
dashboardViewModel: getIt.get<DashboardViewModel>(),
settingsStore: getIt.get<SettingsStore>()));
getIt.registerFactory<DashboardPage>(() => DashboardPage(
bottomSheetService: getIt.get<BottomSheetService>(),
balancePage: getIt.get<BalancePage>(),
dashboardViewModel: getIt.get<DashboardViewModel>(),
addressListViewModel: getIt.get<WalletAddressListViewModel>(),
@@ -459,6 +486,7 @@ Future<void> setup({
});
getIt.registerFactoryParam<DesktopDashboardPage, GlobalKey<NavigatorState>, void>(
(desktopKey, _) => DesktopDashboardPage(
bottomSheetService: getIt.get<BottomSheetService>(),
balancePage: getIt.get<BalancePage>(),
dashboardViewModel: getIt.get<DashboardViewModel>(),
addressListViewModel: getIt.get<WalletAddressListViewModel>(),
@@ -668,7 +696,9 @@ Future<void> setup({
return NodeListViewModel(_nodeSource, appStore);
});
getIt.registerFactory(() => ConnectionSyncPage(getIt.get<DashboardViewModel>()));
getIt.registerFactory(
() => ConnectionSyncPage(getIt.get<DashboardViewModel>(), getIt.get<Web3WalletService>()),
);
getIt.registerFactory(
() => SecurityBackupPage(getIt.get<SecuritySettingsViewModel>(), getIt.get<AuthService>()));
@@ -851,9 +881,8 @@ Future<void> setup({
getIt.registerFactory(() => SupportPage(getIt.get<SupportViewModel>()));
getIt.registerFactory(() =>
SupportChatPage(
getIt.get<SupportViewModel>(), secureStorage: getIt.get<FlutterSecureStorage>()));
getIt.registerFactory(() => SupportChatPage(getIt.get<SupportViewModel>(),
secureStorage: getIt.get<FlutterSecureStorage>()));
getIt.registerFactory(() => SupportOtherLinksPage(getIt.get<SupportViewModel>()));
+2 -4
View File
@@ -15,8 +15,7 @@ class PreferencesKey {
static const disableSellKey = 'disable_sell';
static const defaultBuyProvider = 'default_buy_provider';
static const currentFiatApiModeKey = 'current_fiat_api_mode';
static const allowBiometricalAuthenticationKey =
'allow_biometrical_authentication';
static const allowBiometricalAuthenticationKey = 'allow_biometrical_authentication';
static const useTOTP2FA = 'use_totp_2fa';
static const failedTotpTokenTrials = 'failed_token_trials';
static const disableExchangeKey = 'disable_exchange';
@@ -54,8 +53,7 @@ class PreferencesKey {
static const clearnetDonationLink = 'clearnet_donation_link';
static const onionDonationLink = 'onion_donation_link';
static const lastSeenAppVersion = 'last_seen_app_version';
static const shouldShowMarketPlaceInDashboard =
'should_show_marketplace_in_dashboard';
static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard';
static const isNewInstall = 'is_new_install';
static const shouldRequireTOTP2FAForAccessingWallet =
'should_require_totp_2fa_for_accessing_wallets';
+14
View File
@@ -33,6 +33,20 @@ class CWEthereum extends Ethereum {
@override
String getAddress(WalletBase wallet) => (wallet as EthereumWallet).walletAddresses.address;
@override
String getPrivateKey(WalletBase wallet) {
final privateKeyHolder = (wallet as EthereumWallet).ethPrivateKey;
String stringKey = bytesToHex(privateKeyHolder.privateKey);
return stringKey;
}
@override
String getPublicKey(WalletBase wallet) {
final privateKeyInUnitInt = (wallet as EthereumWallet).ethPrivateKey;
final publicKey = privateKeyInUnitInt.address.hex;
return publicKey;
}
@override
TransactionPriority getDefaultTransactionPriority() => EthereumTransactionPriority.medium;
+10 -11
View File
@@ -39,7 +39,6 @@ import 'package:cake_wallet/src/screens/root/root.dart';
import 'package:uni_links/uni_links.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/wallet_type_utils.dart';
import 'package:cw_core/cake_hive.dart';
final navigatorKey = GlobalKey<NavigatorState>();
@@ -155,7 +154,7 @@ Future<void> initializeAppConfigs() async {
secureStorage: secureStorage,
anonpayInvoiceInfo: anonpayInvoiceInfo,
initialMigrationVersion: 21);
}
}
Future<void> initialSetup(
{required SharedPreferences sharedPreferences,
@@ -308,26 +307,26 @@ class _Home extends StatefulWidget {
}
class _HomeState extends State<_Home> {
@override
@override
void didChangeDependencies() {
if(!ResponsiveLayoutUtil.instance.isMobile){
_setOrientation(context);
if (!ResponsiveLayoutUtil.instance.isMobile) {
_setOrientation(context);
}
super.didChangeDependencies();
}
void _setOrientation(BuildContext context){
void _setOrientation(BuildContext context) {
final orientation = MediaQuery.of(context).orientation;
final width = MediaQuery.of(context).size.width;
final height = MediaQuery.of(context).size.height;
if (orientation == Orientation.portrait && width < height) {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
} else if (orientation == Orientation.landscape && width > height) {
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
SystemChrome.setPreferredOrientations(
[DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
}
}
}
@override
Widget build(BuildContext context) {
+96 -83
View File
@@ -1,9 +1,12 @@
import 'dart:async';
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/entities/preferences_key.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/main_actions.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/market_place_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart';
import 'package:cake_wallet/src/widgets/gradient_background.dart';
import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart';
import 'package:cake_wallet/utils/device_info.dart';
@@ -35,12 +38,14 @@ import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
class DashboardPage extends StatelessWidget {
DashboardPage({
required this.bottomSheetService,
required this.balancePage,
required this.dashboardViewModel,
required this.addressListViewModel,
});
final BalancePage balancePage;
final BottomSheetService bottomSheetService;
final DashboardViewModel dashboardViewModel;
final WalletAddressListViewModel addressListViewModel;
@@ -55,12 +60,14 @@ class DashboardPage extends StatelessWidget {
} else {
return _DashboardPageView(
balancePage: balancePage,
bottomSheetService: bottomSheetService,
dashboardViewModel: dashboardViewModel,
addressListViewModel: addressListViewModel,
);
}
} else if (ResponsiveLayoutUtil.instance.shouldRenderMobileUI()) {
return _DashboardPageView(
bottomSheetService: bottomSheetService,
balancePage: balancePage,
dashboardViewModel: dashboardViewModel,
addressListViewModel: addressListViewModel,
@@ -76,6 +83,7 @@ class DashboardPage extends StatelessWidget {
class _DashboardPageView extends BasePage {
_DashboardPageView({
required this.bottomSheetService,
required this.balancePage,
required this.dashboardViewModel,
required this.addressListViewModel,
@@ -126,6 +134,7 @@ class _DashboardPageView extends BasePage {
}
final DashboardViewModel dashboardViewModel;
final BottomSheetService bottomSheetService;
final WalletAddressListViewModel addressListViewModel;
int get initialPage => dashboardViewModel.shouldShowMarketPlaceInDashboard ? 1 : 0;
@@ -158,102 +167,106 @@ class _DashboardPageView extends BasePage {
return SafeArea(
minimum: EdgeInsets.only(bottom: 24),
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Observer(
builder: (context) {
return PageView.builder(
controller: controller,
itemCount: pages.length,
itemBuilder: (context, index) => pages[index],
);
},
),
),
Padding(
padding: EdgeInsets.only(bottom: 24, top: 10),
child: Observer(
builder: (context) {
return ExcludeSemantics(
child: SmoothPageIndicator(
child: BottomSheetListener(
bottomSheetService: bottomSheetService,
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Observer(
builder: (context) {
return PageView.builder(
controller: controller,
count: pages.length,
effect: ColorTransitionEffect(
spacing: 6.0,
radius: 6.0,
dotWidth: 6.0,
dotHeight: 6.0,
dotColor: Theme.of(context).indicatorColor,
activeDotColor: Theme.of(context)
.extension<DashboardPageTheme>()!
.indicatorDotTheme
.activeIndicatorColor,
),
),
);
},
itemCount: pages.length,
itemBuilder: (context, index) => pages[index],
);
},
),
),
),
Observer(
builder: (_) {
return ClipRect(
child: Container(
margin: const EdgeInsets.only(left: 16, right: 16),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.0),
border: Border.all(
color: Theme.of(context).extension<BalancePageTheme>()!.cardBorderColor,
width: 1,
Padding(
padding: EdgeInsets.only(bottom: 24, top: 10),
child: Observer(
builder: (context) {
return ExcludeSemantics(
child: SmoothPageIndicator(
controller: controller,
count: pages.length,
effect: ColorTransitionEffect(
spacing: 6.0,
radius: 6.0,
dotWidth: 6.0,
dotHeight: 6.0,
dotColor: Theme.of(context).indicatorColor,
activeDotColor: Theme.of(context)
.extension<DashboardPageTheme>()!
.indicatorDotTheme
.activeIndicatorColor,
),
color:
Theme.of(context).extension<SyncIndicatorTheme>()!.syncedBackgroundColor,
),
);
},
),
),
Observer(
builder: (_) {
return ClipRect(
child: Container(
margin: const EdgeInsets.only(left: 16, right: 16),
child: Container(
padding: EdgeInsets.only(left: 32, right: 32),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: MainActions.all
.where((element) => element.canShow?.call(dashboardViewModel) ?? true)
.map(
(action) => Semantics(
button: true,
enabled: (action.isEnabled?.call(dashboardViewModel) ?? true),
child: ActionButton(
image: Image.asset(
action.image,
height: 24,
width: 24,
color: action.isEnabled?.call(dashboardViewModel) ?? true
? Theme.of(context)
.extension<DashboardPageTheme>()!
.mainActionsIconColor
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50.0),
border: Border.all(
color: Theme.of(context).extension<BalancePageTheme>()!.cardBorderColor,
width: 1,
),
color: Theme.of(context)
.extension<SyncIndicatorTheme>()!
.syncedBackgroundColor,
),
child: Container(
padding: EdgeInsets.only(left: 32, right: 32),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: MainActions.all
.where((element) => element.canShow?.call(dashboardViewModel) ?? true)
.map(
(action) => Semantics(
button: true,
enabled: (action.isEnabled?.call(dashboardViewModel) ?? true),
child: ActionButton(
image: Image.asset(
action.image,
height: 24,
width: 24,
color: action.isEnabled?.call(dashboardViewModel) ?? true
? Theme.of(context)
.extension<DashboardPageTheme>()!
.mainActionsIconColor
: Theme.of(context)
.extension<BalancePageTheme>()!
.labelTextColor,
),
title: action.name(context),
onClick: () async =>
await action.onTap(context, dashboardViewModel),
textColor: action.isEnabled?.call(dashboardViewModel) ?? true
? null
: Theme.of(context)
.extension<BalancePageTheme>()!
.labelTextColor,
),
title: action.name(context),
onClick: () async =>
await action.onTap(context, dashboardViewModel),
textColor: action.isEnabled?.call(dashboardViewModel) ?? true
? null
: Theme.of(context)
.extension<BalancePageTheme>()!
.labelTextColor,
),
),
)
.toList(),
)
.toList(),
),
),
),
),
),
);
},
),
],
);
},
),
],
),
),
);
}
@@ -1,8 +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/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/release_notes/release_notes_screen.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart';
import 'package:cake_wallet/src/screens/yat_emoji_id.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
@@ -19,12 +21,14 @@ import 'package:shared_preferences/shared_preferences.dart';
class DesktopDashboardPage extends StatelessWidget {
DesktopDashboardPage({
required this.balancePage,
required this.bottomSheetService,
required this.dashboardViewModel,
required this.addressListViewModel,
required this.desktopKey,
});
final BalancePage balancePage;
final BottomSheetService bottomSheetService;
final DashboardViewModel dashboardViewModel;
final WalletAddressListViewModel addressListViewModel;
final GlobalKey<NavigatorState> desktopKey;
@@ -36,31 +40,34 @@ class DesktopDashboardPage extends StatelessWidget {
Widget build(BuildContext context) {
_setEffects(context);
return Container(
color: Theme.of(context).colorScheme.background,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 400,
child: balancePage,
),
Flexible(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 500),
child: Navigator(
key: desktopKey,
initialRoute: Routes.desktop_actions,
onGenerateRoute: (settings) => Router.createRoute(settings),
onGenerateInitialRoutes: (NavigatorState navigator, String initialRouteName) {
return [
navigator.widget.onGenerateRoute!(RouteSettings(name: initialRouteName))!
];
},
return BottomSheetListener(
bottomSheetService: bottomSheetService,
child: Container(
color: Theme.of(context).colorScheme.background,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 400,
child: balancePage,
),
Flexible(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 500),
child: Navigator(
key: desktopKey,
initialRoute: Routes.desktop_actions,
onGenerateRoute: (settings) => Router.createRoute(settings),
onGenerateInitialRoutes: (NavigatorState navigator, String initialRouteName) {
return [
navigator.widget.onGenerateRoute!(RouteSettings(name: initialRouteName))!
];
},
),
),
),
),
],
],
),
),
);
}
+9 -7
View File
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:flutter/material.dart';
@@ -97,8 +98,7 @@ class RootState extends State<Root> with WidgetsBindingObserver {
return;
}
if (!_isInactive &&
widget.authenticationStore.state == AuthenticationState.allowed) {
if (!_isInactive && widget.authenticationStore.state == AuthenticationState.allowed) {
setState(() => _setInactive(true));
}
@@ -125,16 +125,15 @@ class RootState extends State<Root> with WidgetsBindingObserver {
return;
} else {
final useTotp = widget.appStore.settingsStore.useTOTP2FA;
final shouldUseTotp2FAToAccessWallets = widget.appStore
.settingsStore.shouldRequireTOTP2FAForAccessingWallet;
final shouldUseTotp2FAToAccessWallets =
widget.appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet;
if (useTotp && shouldUseTotp2FAToAccessWallets) {
_reset();
auth.close(
route: Routes.totpAuthCodePage,
arguments: TotpAuthArgumentsModel(
onTotpAuthenticationFinished:
(bool isAuthenticatedSuccessfully,
TotpAuthCodePageState totpAuth) {
(bool isAuthenticatedSuccessfully, TotpAuthCodePageState totpAuth) {
if (!isAuthenticatedSuccessfully) {
return;
}
@@ -169,7 +168,10 @@ class RootState extends State<Root> with WidgetsBindingObserver {
launchUri = null;
}
return WillPopScope(onWillPop: () async => false, child: widget.child);
return WillPopScope(
onWillPop: () async => false,
child: widget.child,
);
}
void _reset() {
@@ -1,12 +1,15 @@
import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/wallet_connect_button.dart';
import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/settings/sync_mode.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
@@ -15,11 +18,12 @@ import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class ConnectionSyncPage extends BasePage {
ConnectionSyncPage(this.dashboardViewModel);
ConnectionSyncPage(this.dashboardViewModel, this.web3walletService);
@override
String get title => S.current.connection_sync;
final Web3WalletService web3walletService;
final DashboardViewModel dashboardViewModel;
@override
@@ -66,6 +70,20 @@ class ConnectionSyncPage extends BasePage {
handler: (context) => Navigator.of(context).pushNamed(Routes.manageNodes),
),
const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
if (dashboardViewModel.wallet.type == WalletType.ethereum) ...[
WalletConnectTile(
onTap: () async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return WalletConnectConnectionsView(web3walletService: web3walletService);
},
),
);
},
),
const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
]
],
),
);
@@ -0,0 +1,46 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
import 'package:flutter/material.dart';
class WalletConnectTile extends StatelessWidget {
const WalletConnectTile({required this.onTap});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Padding(
padding: EdgeInsets.all(24),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Image.asset(
'assets/images/walletconnect_logo.png',
height: 24,
width: 24,
),
SizedBox(width: 16),
Expanded(
child: Text(
S.current.walletConnect,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
),
Image.asset(
'assets/images/select_arrow.png',
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
)
],
),
),
);
}
}
@@ -0,0 +1,71 @@
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';
class ConnectionWidgetBuilder {
static List<ConnectionWidget> buildFromRequiredNamespaces(
Map<String, RequiredNamespace> requiredNamespaces,
) {
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));
views.add(ConnectionWidget(title: key, info: models));
}
return views;
}
static List<ConnectionWidget> buildFromNamespaces(
String topic,
Map<String, Namespace> namespaces,
Web3Wallet web3wallet,
) {
final List<ConnectionWidget> views = [];
for (final key in namespaces.keys) {
final Namespace ns = namespaces[key]!;
final List<ConnectionModel> 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,
));
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'),
);
};
}
models.add(
ConnectionModel(title: S.current.events, elements: ns.events, elementActions: actions),
);
views.add(ConnectionWidget(title: key, info: models));
}
return views;
}
}
@@ -0,0 +1,16 @@
import 'dart:convert';
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;
}
}
@@ -0,0 +1,142 @@
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/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.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 'widgets/pairing_item_widget.dart';
import 'wc_pairing_detail_page.dart';
class WalletConnectConnectionsView extends StatelessWidget {
final Web3WalletService web3walletService;
WalletConnectConnectionsView({required this.web3walletService, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return WCPairingsWidget(web3walletService: web3walletService);
}
}
class WCPairingsWidget extends BasePage {
WCPairingsWidget({required this.web3walletService, Key? key})
: web3wallet = web3walletService.getWeb3Wallet();
final Web3Wallet web3wallet;
final Web3WalletService web3walletService;
@override
String get title => S.current.walletConnect;
Future<void> _onScanQrCode(BuildContext context, Web3Wallet web3Wallet) async {
final String? uri = await presentQRScanner();
if (uri == null) return _invalidUriToast(context, S.current.nullURIError);
try {
log('_onFoundUri: $uri');
final Uri uriData = Uri.parse(uri);
await web3Wallet.pair(uri: uriData);
} on WalletConnectError catch (e) {
await _invalidUriToast(context, e.message);
} catch (e) {
await _invalidUriToast(context, e.toString());
}
}
Future<void> _invalidUriToast(BuildContext context, String message) async {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: message,
buttonText: S.of(context).ok,
buttonAction: Navigator.of(context).pop,
alertBarrierDismissible: false,
);
},
);
}
@override
Widget body(BuildContext context) {
return Observer(
builder: (context) {
return Column(
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
SizedBox(height: 24),
Text(
S.current.connectWalletPrompt,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
SizedBox(height: 16),
PrimaryButton(
text: S.current.newConnection,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
onPressed: () => _onScanQrCode(context, web3wallet),
),
],
),
),
SizedBox(height: 48),
Expanded(
child: Visibility(
visible: web3walletService.pairings.isEmpty,
child: Center(
child: Text(
S.current.activeConnectionsPrompt,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
),
replacement: ListView.builder(
itemCount: web3walletService.pairings.length,
itemBuilder: (BuildContext context, int index) {
final pairing = web3walletService.pairings[index];
return PairingItemWidget(
key: ValueKey(pairing.topic),
pairing: pairing,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WalletConnectPairingDetailsPage(
pairing: pairing,
web3walletService: web3walletService,
),
),
);
},
);
},
),
),
),
SizedBox(height: 48),
],
);
},
);
}
}
@@ -0,0 +1,186 @@
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/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 'utils/namespace_model_builder.dart';
class WalletConnectPairingDetailsPage extends StatefulWidget {
final PairingInfo pairing;
final Web3WalletService web3walletService;
const WalletConnectPairingDetailsPage({
required this.pairing,
required this.web3walletService,
super.key,
});
@override
WalletConnectPairingDetailsPageState createState() => WalletConnectPairingDetailsPageState();
}
class WalletConnectPairingDetailsPageState extends State<WalletConnectPairingDetailsPage> {
List<Widget> sessionWidgets = [];
late String expiryDate;
@override
void initState() {
super.initState();
initDateTime();
initSessions();
}
void initDateTime() {
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(widget.pairing.expiry * 1000);
int year = dateTime.year;
int month = dateTime.month;
int day = dateTime.day;
expiryDate = '$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
}
void initSessions() {
List<SessionData> sessions = widget.web3walletService.getSessionsForPairingInfo(widget.pairing);
for (final SessionData session in sessions) {
List<Widget> namespaceWidget = ConnectionWidgetBuilder.buildFromNamespaces(
session.topic,
session.namespaces,
widget.web3walletService.getWeb3Wallet(),
);
// Loop through and add the namespace widgets, but put 20 pixels between each one
for (int i = 0; i < namespaceWidget.length; i++) {
sessionWidgets.add(namespaceWidget[i]);
if (i != namespaceWidget.length - 1) {
sessionWidgets.add(const SizedBox(height: 20.0));
}
}
}
}
@override
Widget build(BuildContext context) {
return WCCDetailsWidget(
widget.pairing,
expiryDate,
sessionWidgets,
widget.web3walletService,
);
}
}
class WCCDetailsWidget extends BasePage {
WCCDetailsWidget(
this.pairing,
this.expiryDate,
this.sessionWidgets,
this.web3walletService,
);
final PairingInfo pairing;
final String expiryDate;
final List<Widget> sessionWidgets;
final Web3WalletService web3walletService;
@override
Widget body(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.all(8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: CircleAvatar(
backgroundImage: (pairing.peerMetadata!.icons.isNotEmpty
? NetworkImage(pairing.peerMetadata!.icons[0])
: const AssetImage('assets/images/default_icon.png'))
as ImageProvider<Object>,
),
),
const SizedBox(height: 20.0),
Text(
pairing.peerMetadata!.name,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
const SizedBox(height: 16.0),
Text(
pairing.peerMetadata!.url,
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
const SizedBox(height: 8.0),
Text(
'${S.current.expiresOn}: $expiryDate',
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
const SizedBox(height: 20.0),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: sessionWidgets,
),
const SizedBox(height: 20.0),
PrimaryButton(
onPressed: () =>
_onDeleteButtonPressed(context, pairing.peerMetadata!.name, web3walletService),
text: S.current.delete,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
),
],
),
),
),
);
}
Future<void> _onDeleteButtonPressed(
BuildContext context, String dAppName, Web3WalletService web3walletService) async {
bool confirmed = false;
await showPopUp<void>(
context: context,
builder: (BuildContext dialogContext) {
return AlertWithTwoActions(
alertTitle: S.of(context).delete,
alertContent: '${S.current.deleteConnectionConfirmationPrompt} $dAppName?',
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).delete,
actionLeftButton: () => Navigator.of(dialogContext).pop(),
actionRightButton: () {
confirmed = true;
Navigator.of(dialogContext).pop();
},
);
},
);
if (confirmed) {
try {
await web3walletService.disconnectSession(pairing.topic);
Navigator.of(context).pop();
} catch (e) {
log(e.toString());
}
}
}
}
@@ -0,0 +1,102 @@
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);
final ConnectionModel model;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(8),
margin: const EdgeInsetsDirectional.only(top: 8),
child: Visibility(
visible: model.elements != null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
model.title ?? '',
style: TextStyle(
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
if (model.elements != null)
Wrap(
spacing: 4,
runSpacing: 4,
direction: Axis.horizontal,
children: model.elements!
.map((e) => _ModelElementWidget(model: model, modelElement: e))
.toList(),
),
],
),
replacement: _NoModelElementWidget(model: model),
),
);
}
}
class _NoModelElementWidget extends StatelessWidget {
const _NoModelElementWidget({required this.model});
final ConnectionModel model;
@override
Widget build(BuildContext context) {
return Text(
model.text!,
style: TextStyle(
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
fontSize: 14,
fontWeight: FontWeight.w600,
),
);
}
}
class _ModelElementWidget extends StatelessWidget {
const _ModelElementWidget({
required this.model,
required this.modelElement,
});
final ConnectionModel model;
final String modelElement;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: model.elementActions != null ? model.elementActions![modelElement] : null,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(6),
),
padding: const EdgeInsets.all(8),
child: Text(
modelElement,
style: TextStyle(
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
fontSize: 14,
fontWeight: FontWeight.w600,
),
maxLines: 10,
overflow: TextOverflow.ellipsis,
),
),
);
}
}
@@ -0,0 +1,166 @@
// 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,
this.authRequest,
this.sessionProposal,
Key? key,
}) : super(key: key);
final Web3Wallet wallet;
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,
authRequest: widget.authRequest,
sessionProposal: widget.sessionProposal,
wallet: widget.wallet,
);
}
}
class _ConnectionMetadataDisplayWidget extends StatelessWidget {
const _ConnectionMetadataDisplayWidget({
required this.metadata,
required this.wallet,
this.authRequest,
required this.sessionProposal,
});
final ConnectionMetadata? metadata;
final Web3Wallet wallet;
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),
//If authRequest is null, sessionProposal is not null.
replacement: _SessionProposalWidget(sessionProposal: sessionProposal!),
),
],
),
);
}
}
class _AuthRequestWidget extends StatelessWidget {
const _AuthRequestWidget({required this.wallet, this.authRequest});
final Web3Wallet wallet;
final AuthRequestModel? authRequest;
@override
Widget build(BuildContext context) {
final model = ConnectionModel(
text: wallet.formatAuthMessage(
iss: 'did:pkh:eip155:1:${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);
}
}
@@ -0,0 +1,45 @@
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:flutter/material.dart';
import '../../../../core/wallet_connect/models/connection_model.dart';
import 'connection_item_widget.dart';
class ConnectionWidget extends StatelessWidget {
const ConnectionWidget({required this.title, required this.info, super.key});
final String title;
final List<ConnectionModel> info;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).primaryColorLight,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 8),
child: Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
),
const SizedBox(height: 8),
...info.map((e) => ConnectionItemWidget(model: e)),
],
),
);
}
}
@@ -0,0 +1,36 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter/material.dart';
class BottomSheetMessageDisplayWidget extends StatelessWidget {
final String message;
final bool isError;
const BottomSheetMessageDisplayWidget({super.key, required this.message, this.isError = true});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isError ? S.current.error : S.current.successful,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
color: Colors.white,
),
),
SizedBox(height: 8),
Text(
message,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Colors.white,
),
),
],
);
}
}
@@ -0,0 +1,62 @@
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,
);
},
);
item.completer.complete(value);
widget.bottomSheetService.resetCurrentSheet();
}
}
@override
Widget build(BuildContext context) => widget.child;
}
@@ -0,0 +1,48 @@
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,
),
),
],
),
],
),
);
}
}
@@ -0,0 +1,82 @@
import 'package:cake_wallet/generated/i18n.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';
class PairingItemWidget extends StatelessWidget {
const PairingItemWidget({required this.pairing, required this.onTap, super.key});
final PairingInfo pairing;
final void Function() onTap;
@override
Widget build(BuildContext context) {
PairingMetadata? metadata = pairing.peerMetadata;
if (metadata == null) {
return SizedBox.shrink();
}
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(pairing.expiry * 1000);
int year = dateTime.year;
int month = dateTime.month;
int day = dateTime.day;
String expiryDate =
'$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
return ListTile(
leading: CircleAvatar(
backgroundImage: (metadata.icons.isNotEmpty
? NetworkImage(metadata.icons[0])
: const AssetImage(
'assets/images/default_icon.png',
)) as ImageProvider<Object>,
),
title: Text(
metadata.name,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w700,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
metadata.url,
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w700,
color: Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor,
),
),
Text(
'${S.current.expiresOn}: $expiryDate',
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w700,
color: Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor,
),
),
],
),
trailing: Container(
height: 40,
width: 44,
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsBackgroundColor,
),
child: Icon(
Icons.edit,
size: 14,
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
),
),
onTap: onTap,
);
}
}
+1 -2
View File
@@ -595,8 +595,7 @@ abstract class SettingsStoreBase with Store {
SortBalanceBy.values[sharedPreferences.getInt(PreferencesKey.sortBalanceBy) ?? 0];
final pinNativeTokenAtTop =
sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop) ?? true;
final useEtherscan =
sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true;
final useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true;
// If no value
if (pinLength == null || pinLength == 0) {
+124 -128
View File
@@ -47,70 +47,70 @@ abstract class DashboardViewModelBase with Store {
required this.yatStore,
required this.ordersStore,
required this.anonpayTransactionsStore})
: isOutdatedElectrumWallet = false,
hasSellAction = false,
hasBuyAction = false,
hasExchangeAction = false,
isShowFirstYatIntroduction = false,
isShowSecondYatIntroduction = false,
isShowThirdYatIntroduction = false,
filterItems = {
S.current.transactions: [
FilterItem(
value: () => transactionFilterStore.displayAll,
caption: S.current.all_transactions,
onChanged: transactionFilterStore.toggleAll),
FilterItem(
value: () => transactionFilterStore.displayIncoming,
caption: S.current.incoming,
onChanged:transactionFilterStore.toggleIncoming),
FilterItem(
value: () => transactionFilterStore.displayOutgoing,
caption: S.current.outgoing,
onChanged: transactionFilterStore.toggleOutgoing),
// FilterItem(
// value: () => false,
// caption: S.current.transactions_by_date,
// onChanged: null),
],
S.current.trades: [
FilterItem(
value: () => tradeFilterStore.displayAllTrades,
caption: S.current.all_trades,
onChanged: () => tradeFilterStore
.toggleDisplayExchange(ExchangeProviderDescription.all)),
FilterItem(
value: () => tradeFilterStore.displayChangeNow,
caption: ExchangeProviderDescription.changeNow.title,
onChanged: () => tradeFilterStore
.toggleDisplayExchange(ExchangeProviderDescription.changeNow)),
FilterItem(
value: () => tradeFilterStore.displaySideShift,
caption: ExchangeProviderDescription.sideShift.title,
onChanged: () => tradeFilterStore
.toggleDisplayExchange(ExchangeProviderDescription.sideShift)),
FilterItem(
value: () => tradeFilterStore.displaySimpleSwap,
caption: ExchangeProviderDescription.simpleSwap.title,
onChanged: () => tradeFilterStore
.toggleDisplayExchange(ExchangeProviderDescription.simpleSwap)),
FilterItem(
value: () => tradeFilterStore.displayTrocador,
caption: ExchangeProviderDescription.trocador.title,
onChanged: () => tradeFilterStore
.toggleDisplayExchange(ExchangeProviderDescription.trocador)),
FilterItem(
value: () => tradeFilterStore.displayExolix,
caption: ExchangeProviderDescription.exolix.title,
onChanged: () => tradeFilterStore
.toggleDisplayExchange(ExchangeProviderDescription.exolix)),
]
},
subname = '',
name = appStore.wallet!.name,
type = appStore.wallet!.type,
transactions = ObservableList<TransactionListItem>(),
wallet = appStore.wallet! {
: isOutdatedElectrumWallet = false,
hasSellAction = false,
hasBuyAction = false,
hasExchangeAction = false,
isShowFirstYatIntroduction = false,
isShowSecondYatIntroduction = false,
isShowThirdYatIntroduction = false,
filterItems = {
S.current.transactions: [
FilterItem(
value: () => transactionFilterStore.displayAll,
caption: S.current.all_transactions,
onChanged: transactionFilterStore.toggleAll),
FilterItem(
value: () => transactionFilterStore.displayIncoming,
caption: S.current.incoming,
onChanged: transactionFilterStore.toggleIncoming),
FilterItem(
value: () => transactionFilterStore.displayOutgoing,
caption: S.current.outgoing,
onChanged: transactionFilterStore.toggleOutgoing),
// FilterItem(
// value: () => false,
// caption: S.current.transactions_by_date,
// onChanged: null),
],
S.current.trades: [
FilterItem(
value: () => tradeFilterStore.displayAllTrades,
caption: S.current.all_trades,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.all)),
FilterItem(
value: () => tradeFilterStore.displayChangeNow,
caption: ExchangeProviderDescription.changeNow.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.changeNow)),
FilterItem(
value: () => tradeFilterStore.displaySideShift,
caption: ExchangeProviderDescription.sideShift.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.sideShift)),
FilterItem(
value: () => tradeFilterStore.displaySimpleSwap,
caption: ExchangeProviderDescription.simpleSwap.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.simpleSwap)),
FilterItem(
value: () => tradeFilterStore.displayTrocador,
caption: ExchangeProviderDescription.trocador.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.trocador)),
FilterItem(
value: () => tradeFilterStore.displayExolix,
caption: ExchangeProviderDescription.exolix.title,
onChanged: () =>
tradeFilterStore.toggleDisplayExchange(ExchangeProviderDescription.exolix)),
]
},
subname = '',
name = appStore.wallet!.name,
type = appStore.wallet!.type,
transactions = ObservableList<TransactionListItem>(),
wallet = appStore.wallet! {
name = wallet.name;
type = wallet.type;
isOutdatedElectrumWallet =
@@ -125,15 +125,17 @@ abstract class DashboardViewModelBase with Store {
if (_wallet.type == WalletType.monero) {
subname = monero!.getCurrentAccount(_wallet).label;
_onMoneroAccountChangeReaction = reaction((_) => monero!.getMoneroWalletDetails(wallet)
.account, (Account account) => _onMoneroAccountChange(_wallet));
_onMoneroAccountChangeReaction = reaction(
(_) => monero!.getMoneroWalletDetails(wallet).account,
(Account account) => _onMoneroAccountChange(_wallet));
_onMoneroBalanceChangeReaction = reaction((_) => monero!.getMoneroWalletDetails(wallet).balance,
_onMoneroBalanceChangeReaction = reaction(
(_) => monero!.getMoneroWalletDetails(wallet).balance,
(MoneroBalance balance) => _onMoneroTransactionsUpdate(_wallet));
final _accountTransactions = _wallet
.transactionHistory.transactions.values
.where((tx) => monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id)
final _accountTransactions = _wallet.transactionHistory.transactions.values
.where((tx) =>
monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id)
.toList();
transactions = ObservableList.of(_accountTransactions.map((transaction) =>
@@ -142,34 +144,33 @@ abstract class DashboardViewModelBase with Store {
balanceViewModel: balanceViewModel,
settingsStore: appStore.settingsStore)));
} else {
transactions = ObservableList.of(wallet
.transactionHistory.transactions.values
.map((transaction) => TransactionListItem(
transactions = ObservableList.of(wallet.transactionHistory.transactions.values.map(
(transaction) => TransactionListItem(
transaction: transaction,
balanceViewModel: balanceViewModel,
settingsStore: appStore.settingsStore)));
}
reaction((_) => appStore.wallet, _onWalletChange);
connectMapToListWithTransform(
appStore.wallet!.transactionHistory.transactions,
transactions,
(TransactionInfo? transaction) => TransactionListItem(
transaction: transaction!,
balanceViewModel: balanceViewModel,
settingsStore: appStore.settingsStore),
filter: (TransactionInfo? transaction) {
if (transaction == null) {
return false;
}
settingsStore: appStore.settingsStore), filter: (TransactionInfo? transaction) {
if (transaction == null) {
return false;
}
final wallet = _wallet;
if (wallet.type == WalletType.monero) {
return monero!.getTransactionInfoAccountId(transaction) == monero!.getCurrentAccount(wallet).id;
}
final wallet = _wallet;
if (wallet.type == WalletType.monero) {
return monero!.getTransactionInfoAccountId(transaction) ==
monero!.getCurrentAccount(wallet).id;
}
return true;
return true;
});
}
@@ -216,24 +217,21 @@ abstract class DashboardViewModelBase with Store {
}
@computed
BalanceDisplayMode get balanceDisplayMode =>
appStore.settingsStore.balanceDisplayMode;
BalanceDisplayMode get balanceDisplayMode => appStore.settingsStore.balanceDisplayMode;
@computed
bool get shouldShowMarketPlaceInDashboard {
return appStore.settingsStore.shouldShowMarketPlaceInDashboard;
}
@computed
List<TradeListItem> get trades => tradesStore.trades
.where((trade) => trade.trade.walletId == wallet.id)
.toList();
List<TradeListItem> get trades =>
tradesStore.trades.where((trade) => trade.trade.walletId == wallet.id).toList();
@computed
List<OrderListItem> get orders => ordersStore.orders
.where((item) => item.order.walletId == wallet.id)
.toList();
List<OrderListItem> get orders =>
ordersStore.orders.where((item) => item.order.walletId == wallet.id).toList();
@computed
List<AnonpayTransactionListItem> get anonpayTransactons => anonpayTransactionsStore.transactions
.where((item) => item.transaction.walletId == wallet.id)
@@ -250,7 +248,8 @@ abstract class DashboardViewModelBase with Store {
List<ActionListItem> get items {
final _items = <ActionListItem>[];
_items.addAll(transactionFilterStore.filtered(transactions: [...transactions, ...anonpayTransactons]));
_items.addAll(
transactionFilterStore.filtered(transactions: [...transactions, ...anonpayTransactons]));
_items.addAll(tradeFilterStore.filtered(trades: trades, wallet: wallet));
_items.addAll(orders);
@@ -258,8 +257,7 @@ abstract class DashboardViewModelBase with Store {
}
@observable
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>
wallet;
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
bool get hasRescan => wallet.type == WalletType.monero || wallet.type == WalletType.haven;
@@ -283,7 +281,6 @@ abstract class DashboardViewModelBase with Store {
Map<String, List<FilterItem>> filterItems;
BuyProviderType get defaultBuyProvider => settingsStore.defaultBuyProvider;
bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;
@@ -291,8 +288,7 @@ abstract class DashboardViewModelBase with Store {
bool get shouldShowYatPopup => settingsStore.shouldShowYatPopup;
@action
void furtherShowYatPopup(bool shouldShow) =>
settingsStore.shouldShowYatPopup = shouldShow;
void furtherShowYatPopup(bool shouldShow) => settingsStore.shouldShowYatPopup = shouldShow;
@computed
bool get isEnabledExchangeAction => settingsStore.exchangeStatus != ExchangeApiMode.disabled;
@@ -301,8 +297,7 @@ abstract class DashboardViewModelBase with Store {
bool hasExchangeAction;
@computed
bool get isEnabledBuyAction =>
!settingsStore.disableBuy && wallet.type != WalletType.haven;
bool get isEnabledBuyAction => !settingsStore.disableBuy && wallet.type != WalletType.haven;
@observable
bool hasBuyAction;
@@ -330,9 +325,7 @@ abstract class DashboardViewModelBase with Store {
@action
void _onWalletChange(
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
TransactionInfo>?
wallet) {
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>? wallet) {
if (wallet == null) {
return;
}
@@ -350,10 +343,12 @@ abstract class DashboardViewModelBase with Store {
_onMoneroAccountChangeReaction?.reaction.dispose();
_onMoneroBalanceChangeReaction?.reaction.dispose();
_onMoneroAccountChangeReaction = reaction((_) => monero!.getMoneroWalletDetails(wallet)
.account, (Account account) => _onMoneroAccountChange(wallet));
_onMoneroAccountChangeReaction = reaction(
(_) => monero!.getMoneroWalletDetails(wallet).account,
(Account account) => _onMoneroAccountChange(wallet));
_onMoneroBalanceChangeReaction = reaction((_) => monero!.getMoneroWalletDetails(wallet).balance,
_onMoneroBalanceChangeReaction = reaction(
(_) => monero!.getMoneroWalletDetails(wallet).balance,
(MoneroBalance balance) => _onMoneroTransactionsUpdate(wallet));
_onMoneroTransactionsUpdate(wallet);
@@ -364,8 +359,8 @@ abstract class DashboardViewModelBase with Store {
transactions.clear();
transactions.addAll(wallet.transactionHistory.transactions.values.map(
(transaction) => TransactionListItem(
transactions.addAll(wallet.transactionHistory.transactions.values.map((transaction) =>
TransactionListItem(
transaction: transaction,
balanceViewModel: balanceViewModel,
settingsStore: appStore.settingsStore)));
@@ -374,21 +369,19 @@ abstract class DashboardViewModelBase with Store {
connectMapToListWithTransform(
appStore.wallet!.transactionHistory.transactions,
transactions,
(TransactionInfo? transaction)
=> TransactionListItem(
(TransactionInfo? transaction) => TransactionListItem(
transaction: transaction!,
balanceViewModel: balanceViewModel,
settingsStore: appStore.settingsStore),
filter: (TransactionInfo? tx) {
if (tx == null) {
return false;
}
settingsStore: appStore.settingsStore), filter: (TransactionInfo? tx) {
if (tx == null) {
return false;
}
if (wallet.type == WalletType.monero) {
return monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id;
}
if (wallet.type == WalletType.monero) {
return monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id;
}
return true;
return true;
});
}
@@ -402,15 +395,18 @@ abstract class DashboardViewModelBase with Store {
void _onMoneroTransactionsUpdate(WalletBase wallet) {
transactions.clear();
final _accountTransactions = monero!.getTransactionHistory(wallet).transactions.values
.where((tx) => monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id)
final _accountTransactions = monero!
.getTransactionHistory(wallet)
.transactions
.values
.where(
(tx) => monero!.getTransactionInfoAccountId(tx) == monero!.getCurrentAccount(wallet).id)
.toList();
transactions.addAll(_accountTransactions.map((transaction) =>
TransactionListItem(
transaction: transaction,
balanceViewModel: balanceViewModel,
settingsStore: appStore.settingsStore)));
transactions.addAll(_accountTransactions.map((transaction) => TransactionListItem(
transaction: transaction,
balanceViewModel: balanceViewModel,
settingsStore: appStore.settingsStore)));
}
void updateActions() {
+1 -1
View File
@@ -417,7 +417,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
String translateErrorMessage(String error, WalletType walletType, CryptoCurrency currency,) {
if (walletType == WalletType.ethereum || walletType == WalletType.haven) {
if (error.contains('gas required exceeds allowance (0)') || error.contains('insufficient funds for gas')) {
if (error.contains('gas required exceeds allowance') || error.contains('insufficient funds for gas')) {
return S.current.do_not_have_enough_gas_asset(currency.toString());
}
}