From 3dd6351ae2191f1c1c76110a970fc7791309a4b7 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Wed, 4 Jan 2023 16:51:23 +0200 Subject: [PATCH] Complete Ethereum wallet creation flow --- assets/ethereum_server_list.yml | 2 + cw_core/lib/currency_for_wallet_type.dart | 2 + cw_ethereum/lib/ethereum_balance.dart | 18 ++-- cw_ethereum/lib/ethereum_client.dart | 25 ++++++ cw_ethereum/lib/ethereum_wallet.dart | 84 ++++++++++++++++--- cw_ethereum/pubspec.yaml | 3 +- lib/entities/default_settings_migration.dart | 42 ++++++++++ lib/entities/node_list.dart | 16 ++++ lib/entities/preferences_key.dart | 1 + .../dashboard/widgets/menu_widget.dart | 8 +- lib/store/settings_store.dart | 18 ++++ 11 files changed, 197 insertions(+), 22 deletions(-) create mode 100644 assets/ethereum_server_list.yml create mode 100644 cw_ethereum/lib/ethereum_client.dart diff --git a/assets/ethereum_server_list.yml b/assets/ethereum_server_list.yml new file mode 100644 index 000000000..805e9af60 --- /dev/null +++ b/assets/ethereum_server_list.yml @@ -0,0 +1,2 @@ +- + uri: 10.0.2.2:7545 \ No newline at end of file diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index 3904fc049..8ac8c1fc6 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -11,6 +11,8 @@ CryptoCurrency currencyForWalletType(WalletType type) { return CryptoCurrency.ltc; case WalletType.haven: return CryptoCurrency.xhv; + case WalletType.ethereum: + return CryptoCurrency.eth; default: throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); } diff --git a/cw_ethereum/lib/ethereum_balance.dart b/cw_ethereum/lib/ethereum_balance.dart index 634aff766..433b5bb10 100644 --- a/cw_ethereum/lib/ethereum_balance.dart +++ b/cw_ethereum/lib/ethereum_balance.dart @@ -1,13 +1,21 @@ +import 'dart:convert'; + import 'package:cw_core/balance.dart'; +import 'package:web3dart/web3dart.dart'; class EthereumBalance extends Balance { EthereumBalance(super.available, super.additional); @override - // TODO: implement formattedAdditionalBalance - String get formattedAdditionalBalance => throw UnimplementedError(); + String get formattedAdditionalBalance { + return EtherAmount.fromUnitAndValue(EtherUnit.ether, additional.toString()) + .getInEther + .toString(); + } @override - // TODO: implement formattedAvailableBalance - String get formattedAvailableBalance => throw UnimplementedError(); -} \ No newline at end of file + String get formattedAvailableBalance => + EtherAmount.fromUnitAndValue(EtherUnit.ether, available.toString()).getInEther.toString(); + + String toJSON() => json.encode({'available': available, 'additional': additional}); +} diff --git a/cw_ethereum/lib/ethereum_client.dart b/cw_ethereum/lib/ethereum_client.dart new file mode 100644 index 000000000..d73dba715 --- /dev/null +++ b/cw_ethereum/lib/ethereum_client.dart @@ -0,0 +1,25 @@ +import 'package:cw_core/node.dart'; +import 'package:http/http.dart'; +import 'package:web3dart/web3dart.dart'; + +class EthereumClient { + late final Web3Client _client; + + Future connect(Node node) async { + try { + _client = Web3Client(node.uriRaw, Client()); + + return true; + } catch (e) { + return false; + } + } + + Future getBalance(String privateKey) async { + final private = EthPrivateKey.fromHex(privateKey); + + return _client.getBalance(private.address); + } + + Future getGasPrice() async => _client.getGasPrice(); +} \ No newline at end of file diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart index f350c628a..53ab87e9e 100644 --- a/cw_ethereum/lib/ethereum_wallet.dart +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:cw_core/crypto_currency.dart'; @@ -10,11 +11,13 @@ import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_ethereum/ethereum_balance.dart'; +import 'package:cw_ethereum/ethereum_client.dart'; import 'package:cw_ethereum/ethereum_transaction_history.dart'; import 'package:cw_ethereum/ethereum_transaction_info.dart'; import 'package:cw_ethereum/ethereum_wallet_addresses.dart'; import 'package:cw_ethereum/file.dart'; import 'package:mobx/mobx.dart'; +import 'package:web3dart/web3dart.dart'; part 'ethereum_wallet.g.dart'; @@ -28,9 +31,12 @@ abstract class EthereumWalletBase required this.mnemonic, required this.privateKey, required String password, + EthereumBalance? initialBalance, }) : syncStatus = NotConnectedSyncStatus(), _password = password, walletAddresses = EthereumWalletAddresses(walletInfo), + balance = ObservableMap.of( + {CryptoCurrency.eth: initialBalance ?? EthereumBalance(0, 0)}), super(walletInfo) { this.walletInfo = walletInfo; transactionHistory = EthereumTransactionHistory(); @@ -40,46 +46,68 @@ abstract class EthereumWalletBase final String privateKey; final String _password; + late EthereumClient _client; + + EtherAmount? _gasPrice; + + @override + WalletAddresses walletAddresses; + @override SyncStatus syncStatus; @override - ObservableMap get balance => throw UnimplementedError(); + @observable + late ObservableMap balance; @override int calculateEstimatedFee(TransactionPriority priority, int? amount) { - throw UnimplementedError(); + throw UnimplementedError("calculateEstimatedFee"); } @override Future changePassword(String password) { - throw UnimplementedError(); + throw UnimplementedError("changePassword"); } @override void close() {} @override - Future connectToNode({required Node node}) { - throw UnimplementedError(); + Future connectToNode({required Node node}) async { + try { + syncStatus = ConnectingSyncStatus(); + + final isConnected = await _client.connect(node); + + if (!isConnected) { + throw Exception("Ethereum Node connection failed"); + } + + _updateBalance(); + + syncStatus = ConnectedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } } @override Future createTransaction(Object credentials) { - throw UnimplementedError(); + throw UnimplementedError("createTransaction"); } @override Future> fetchTransactions() { - throw UnimplementedError(); + throw UnimplementedError("fetchTransactions"); } @override - Object get keys => throw UnimplementedError(); + Object get keys => throw UnimplementedError("keys"); @override Future rescan({required int height}) { - throw UnimplementedError(); + throw UnimplementedError("rescan"); } @override @@ -93,17 +121,47 @@ abstract class EthereumWalletBase String get seed => mnemonic; @override - Future startSync() { - throw UnimplementedError(); + Future startSync() async { + try { + syncStatus = AttemptingSyncStatus(); + await _updateBalance(); + _gasPrice = await _client.getGasPrice(); + + Timer.periodic( + const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasPrice()); + + syncStatus = SyncedSyncStatus(); + } catch (e, stacktrace) { + print(stacktrace); + print(e.toString()); + syncStatus = FailedSyncStatus(); + } } - @override - WalletAddresses walletAddresses; + int feeRate() { + if (_gasPrice != null) { + return _gasPrice!.getInEther.toInt(); + } + + return 0; + } Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); String toJSON() => json.encode({ 'mnemonic': mnemonic, + 'balance': balance[currency]!.toJSON(), // TODO: save other attributes }); + + Future _updateBalance() async { + balance[currency] = await _fetchBalances(); + await save(); + } + + Future _fetchBalances() async { + final balance = await _client.getBalance(privateKey); + + return EthereumBalance(balance.getInEther.toInt(), balance.getInEther.toInt()); + } } diff --git a/cw_ethereum/pubspec.yaml b/cw_ethereum/pubspec.yaml index b004cd14d..982a422a0 100644 --- a/cw_ethereum/pubspec.yaml +++ b/cw_ethereum/pubspec.yaml @@ -12,11 +12,12 @@ environment: dependencies: flutter: sdk: flutter -# web3dart: ^2.4.1 + web3dart: ^2.4.1 mobx: ^2.0.7+4 bip39: ^1.0.6 ed25519_hd_key: ^2.2.0 hex: ^0.2.0 + http: ^0.13.4 cw_core: path: ../cw_core diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 3c8d9fbbe..990909c87 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -24,6 +24,7 @@ const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081'; const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002'; const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002'; const havenDefaultNodeUri = 'nodes.havenprotocol.org:443'; +const ethereumDefaultNodeUri = '10.0.2.2:7545'; Future defaultSettingsMigration( {required int version, @@ -143,6 +144,12 @@ Future defaultSettingsMigration( await validateBitcoinSavedTransactionPriority(sharedPreferences); break; + case 20: + await addEthereumNodeList(nodes: nodes); + await changeEthereumCurrentNodeToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); + break; + default: break; } @@ -228,6 +235,12 @@ Node? getHavenDefaultNode({required Box nodes}) { ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.haven); } +Node? getEthereumDefaultNode({required Box nodes}) { + return nodes.values.firstWhereOrNull( + (Node node) => node.uriRaw == ethereumDefaultNodeUri) + ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.ethereum); +} + Node getMoneroDefaultNode({required Box nodes}) { final timeZone = DateTime.now().timeZoneOffset.inHours; var nodeUri = ''; @@ -424,6 +437,8 @@ Future checkCurrentNodes( .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); final currentHavenNodeId = sharedPreferences .getInt(PreferencesKey.currentHavenNodeIdKey); + final currentEthereumNodeId = sharedPreferences + .getInt(PreferencesKey.currentEthereumNodeIdKey); final currentMoneroNode = nodeSource.values.firstWhereOrNull( (node) => node.key == currentMoneroNodeId); final currentBitcoinElectrumServer = nodeSource.values.firstWhereOrNull( @@ -432,6 +447,8 @@ Future checkCurrentNodes( (node) => node.key == currentLitecoinElectrumSeverId); final currentHavenNodeServer = nodeSource.values.firstWhereOrNull( (node) => node.key == currentHavenNodeId); + final currentEthereumNodeServer = nodeSource.values.firstWhereOrNull( + (node) => node.key == currentEthereumNodeId); if (currentMoneroNode == null) { final newCakeWalletNode = @@ -465,6 +482,13 @@ Future checkCurrentNodes( await sharedPreferences.setInt( PreferencesKey.currentHavenNodeIdKey, node.key as int); } + + if (currentEthereumNodeServer == null) { + final node = Node(uri: ethereumDefaultNodeUri, type: WalletType.ethereum); + await nodeSource.add(node); + await sharedPreferences.setInt( + PreferencesKey.currentEthereumNodeIdKey, node.key as int); + } } Future resetBitcoinElectrumServer( @@ -501,3 +525,21 @@ Future changeDefaultHavenNode( await node.save(); }); } + +Future addEthereumNodeList({required Box nodes}) async { + final nodeList = await loadDefaultEthereumNodes(); + for (var node in nodeList) { + if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { + await nodes.add(node); + } + } +} + +Future changeEthereumCurrentNodeToDefault( + {required SharedPreferences sharedPreferences, + required Box nodes}) async { + final node = getEthereumDefaultNode(nodes: nodes); + final nodeId = node?.key as int? ?? 0; + + await sharedPreferences.setInt(PreferencesKey.currentEthereumNodeIdKey, nodeId); +} diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 58847ccfa..b06351a79 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -70,6 +70,22 @@ Future> loadDefaultHavenNodes() async { return nodes; } +Future> loadDefaultEthereumNodes() async { + final nodesRaw = await rootBundle.loadString('assets/ethereum_server_list.yml'); + final loadedNodes = loadYaml(nodesRaw) as YamlList; + final nodes = []; + + for (final raw in loadedNodes) { + if (raw is Map) { + final node = Node.fromMap(Map.from(raw)); + node.type = WalletType.ethereum; + nodes.add(node); + } + } + + return nodes; +} + Future resetToDefault(Box nodeSource) async { final moneroNodes = await loadDefaultNodes(); final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 90d4dcad7..b9ffc2aff 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -5,6 +5,7 @@ class PreferencesKey { static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc'; static const currentLitecoinElectrumSererIdKey = 'current_node_id_ltc'; static const currentHavenNodeIdKey = 'current_node_id_xhv'; + static const currentEthereumNodeIdKey = 'current_node_id_eth'; static const currentFiatCurrencyKey = 'current_fiat_currency'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 87bf956b5..06e79251e 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -30,7 +30,8 @@ class MenuWidgetState extends State { this.moneroIcon = Image.asset('assets/images/monero_menu.png'), this.bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png'), this.litecoinIcon = Image.asset('assets/images/litecoin_menu.png'), - this.havenIcon = Image.asset('assets/images/haven_menu.png'); + this.havenIcon = Image.asset('assets/images/haven_menu.png'), + this.ethereumIcon = Image.asset('assets/images/eth_icon.png'); final largeScreen = 731; @@ -47,6 +48,7 @@ class MenuWidgetState extends State { Image bitcoinIcon; Image litecoinIcon; Image havenIcon; + Image ethereumIcon; @override void initState() { @@ -92,8 +94,6 @@ class MenuWidgetState extends State { color: Theme.of(context).accentTextTheme!.overline!.decorationColor!); bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png', color: Theme.of(context).accentTextTheme!.overline!.decorationColor!); - litecoinIcon = Image.asset('assets/images/litecoin_menu.png'); - havenIcon = Image.asset('assets/images/haven_menu.png'); return Row( mainAxisSize: MainAxisSize.max, @@ -259,6 +259,8 @@ class MenuWidgetState extends State { return litecoinIcon; case WalletType.haven: return havenIcon; + case WalletType.ethereum: + return ethereumIcon; default: throw Exception('No icon for ${type.toString()}'); } diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index b10e0d08d..081cb33a6 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -318,10 +318,13 @@ abstract class SettingsStoreBase with Store { .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); final havenNodeId = sharedPreferences .getInt(PreferencesKey.currentHavenNodeIdKey); + final ethereumNodeId = sharedPreferences + .getInt(PreferencesKey.currentEthereumNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); final havenNode = nodeSource.get(havenNodeId); + final ethereumNode = nodeSource.get(ethereumNodeId); final packageInfo = await PackageInfo.fromPlatform(); final shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true; @@ -344,6 +347,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.haven] = havenNode; } + if (ethereumNode != null) { + nodes[WalletType.ethereum] = ethereumNode; + } + return SettingsStore( sharedPreferences: sharedPreferences, nodes: nodes, @@ -429,10 +436,13 @@ abstract class SettingsStoreBase with Store { .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); final havenNodeId = sharedPreferences .getInt(PreferencesKey.currentHavenNodeIdKey); + final ethereumNodeId = sharedPreferences + .getInt(PreferencesKey.currentEthereumNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); final havenNode = nodeSource.get(havenNodeId); + final ethereumNode = nodeSource.get(ethereumNodeId); if (moneroNode != null) { nodes[WalletType.monero] = moneroNode; @@ -449,6 +459,10 @@ abstract class SettingsStoreBase with Store { if (havenNode != null) { nodes[WalletType.haven] = havenNode; } + + if (ethereumNode != null) { + nodes[WalletType.ethereum] = ethereumNode; + } } Future _saveCurrentNode(Node node, WalletType walletType) async { @@ -469,6 +483,10 @@ abstract class SettingsStoreBase with Store { await _sharedPreferences.setInt( PreferencesKey.currentHavenNodeIdKey, node.key as int); break; + case WalletType.ethereum: + await _sharedPreferences.setInt( + PreferencesKey.currentEthereumNodeIdKey, node.key as int); + break; default: break; }