Complete Ethereum wallet creation flow

This commit is contained in:
OmarHatem 2023-01-04 16:51:23 +02:00
parent 91ef1fdc7d
commit 3dd6351ae2
11 changed files with 197 additions and 22 deletions

View file

@ -0,0 +1,2 @@
-
uri: 10.0.2.2:7545

View file

@ -11,6 +11,8 @@ CryptoCurrency currencyForWalletType(WalletType type) {
return CryptoCurrency.ltc; return CryptoCurrency.ltc;
case WalletType.haven: case WalletType.haven:
return CryptoCurrency.xhv; return CryptoCurrency.xhv;
case WalletType.ethereum:
return CryptoCurrency.eth;
default: default:
throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType');
} }

View file

@ -1,13 +1,21 @@
import 'dart:convert';
import 'package:cw_core/balance.dart'; import 'package:cw_core/balance.dart';
import 'package:web3dart/web3dart.dart';
class EthereumBalance extends Balance { class EthereumBalance extends Balance {
EthereumBalance(super.available, super.additional); EthereumBalance(super.available, super.additional);
@override @override
// TODO: implement formattedAdditionalBalance String get formattedAdditionalBalance {
String get formattedAdditionalBalance => throw UnimplementedError(); return EtherAmount.fromUnitAndValue(EtherUnit.ether, additional.toString())
.getInEther
.toString();
}
@override @override
// TODO: implement formattedAvailableBalance String get formattedAvailableBalance =>
String get formattedAvailableBalance => throw UnimplementedError(); EtherAmount.fromUnitAndValue(EtherUnit.ether, available.toString()).getInEther.toString();
String toJSON() => json.encode({'available': available, 'additional': additional});
} }

View file

@ -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<bool> connect(Node node) async {
try {
_client = Web3Client(node.uriRaw, Client());
return true;
} catch (e) {
return false;
}
}
Future<EtherAmount> getBalance(String privateKey) async {
final private = EthPrivateKey.fromHex(privateKey);
return _client.getBalance(private.address);
}
Future<EtherAmount> getGasPrice() async => _client.getGasPrice();
}

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:cw_core/crypto_currency.dart'; 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_base.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_ethereum/ethereum_balance.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_history.dart';
import 'package:cw_ethereum/ethereum_transaction_info.dart'; import 'package:cw_ethereum/ethereum_transaction_info.dart';
import 'package:cw_ethereum/ethereum_wallet_addresses.dart'; import 'package:cw_ethereum/ethereum_wallet_addresses.dart';
import 'package:cw_ethereum/file.dart'; import 'package:cw_ethereum/file.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:web3dart/web3dart.dart';
part 'ethereum_wallet.g.dart'; part 'ethereum_wallet.g.dart';
@ -28,9 +31,12 @@ abstract class EthereumWalletBase
required this.mnemonic, required this.mnemonic,
required this.privateKey, required this.privateKey,
required String password, required String password,
EthereumBalance? initialBalance,
}) : syncStatus = NotConnectedSyncStatus(), }) : syncStatus = NotConnectedSyncStatus(),
_password = password, _password = password,
walletAddresses = EthereumWalletAddresses(walletInfo), walletAddresses = EthereumWalletAddresses(walletInfo),
balance = ObservableMap<CryptoCurrency, EthereumBalance>.of(
{CryptoCurrency.eth: initialBalance ?? EthereumBalance(0, 0)}),
super(walletInfo) { super(walletInfo) {
this.walletInfo = walletInfo; this.walletInfo = walletInfo;
transactionHistory = EthereumTransactionHistory(); transactionHistory = EthereumTransactionHistory();
@ -40,46 +46,68 @@ abstract class EthereumWalletBase
final String privateKey; final String privateKey;
final String _password; final String _password;
late EthereumClient _client;
EtherAmount? _gasPrice;
@override
WalletAddresses walletAddresses;
@override @override
SyncStatus syncStatus; SyncStatus syncStatus;
@override @override
ObservableMap<CryptoCurrency, EthereumBalance> get balance => throw UnimplementedError(); @observable
late ObservableMap<CryptoCurrency, EthereumBalance> balance;
@override @override
int calculateEstimatedFee(TransactionPriority priority, int? amount) { int calculateEstimatedFee(TransactionPriority priority, int? amount) {
throw UnimplementedError(); throw UnimplementedError("calculateEstimatedFee");
} }
@override @override
Future<void> changePassword(String password) { Future<void> changePassword(String password) {
throw UnimplementedError(); throw UnimplementedError("changePassword");
} }
@override @override
void close() {} void close() {}
@override @override
Future<void> connectToNode({required Node node}) { Future<void> connectToNode({required Node node}) async {
throw UnimplementedError(); 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 @override
Future<PendingTransaction> createTransaction(Object credentials) { Future<PendingTransaction> createTransaction(Object credentials) {
throw UnimplementedError(); throw UnimplementedError("createTransaction");
} }
@override @override
Future<Map<String, EthereumTransactionInfo>> fetchTransactions() { Future<Map<String, EthereumTransactionInfo>> fetchTransactions() {
throw UnimplementedError(); throw UnimplementedError("fetchTransactions");
} }
@override @override
Object get keys => throw UnimplementedError(); Object get keys => throw UnimplementedError("keys");
@override @override
Future<void> rescan({required int height}) { Future<void> rescan({required int height}) {
throw UnimplementedError(); throw UnimplementedError("rescan");
} }
@override @override
@ -93,17 +121,47 @@ abstract class EthereumWalletBase
String get seed => mnemonic; String get seed => mnemonic;
@override @override
Future<void> startSync() { Future<void> startSync() async {
throw UnimplementedError(); 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 int feeRate() {
WalletAddresses walletAddresses; if (_gasPrice != null) {
return _gasPrice!.getInEther.toInt();
}
return 0;
}
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
String toJSON() => json.encode({ String toJSON() => json.encode({
'mnemonic': mnemonic, 'mnemonic': mnemonic,
'balance': balance[currency]!.toJSON(),
// TODO: save other attributes // TODO: save other attributes
}); });
Future<void> _updateBalance() async {
balance[currency] = await _fetchBalances();
await save();
}
Future<EthereumBalance> _fetchBalances() async {
final balance = await _client.getBalance(privateKey);
return EthereumBalance(balance.getInEther.toInt(), balance.getInEther.toInt());
}
} }

View file

@ -12,11 +12,12 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
# web3dart: ^2.4.1 web3dart: ^2.4.1
mobx: ^2.0.7+4 mobx: ^2.0.7+4
bip39: ^1.0.6 bip39: ^1.0.6
ed25519_hd_key: ^2.2.0 ed25519_hd_key: ^2.2.0
hex: ^0.2.0 hex: ^0.2.0
http: ^0.13.4
cw_core: cw_core:
path: ../cw_core path: ../cw_core

View file

@ -24,6 +24,7 @@ const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081';
const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002'; const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002';
const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002'; const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002';
const havenDefaultNodeUri = 'nodes.havenprotocol.org:443'; const havenDefaultNodeUri = 'nodes.havenprotocol.org:443';
const ethereumDefaultNodeUri = '10.0.2.2:7545';
Future defaultSettingsMigration( Future defaultSettingsMigration(
{required int version, {required int version,
@ -143,6 +144,12 @@ Future defaultSettingsMigration(
await validateBitcoinSavedTransactionPriority(sharedPreferences); await validateBitcoinSavedTransactionPriority(sharedPreferences);
break; break;
case 20:
await addEthereumNodeList(nodes: nodes);
await changeEthereumCurrentNodeToDefault(
sharedPreferences: sharedPreferences, nodes: nodes);
break;
default: default:
break; break;
} }
@ -228,6 +235,12 @@ Node? getHavenDefaultNode({required Box<Node> nodes}) {
?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.haven); ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.haven);
} }
Node? getEthereumDefaultNode({required Box<Node> nodes}) {
return nodes.values.firstWhereOrNull(
(Node node) => node.uriRaw == ethereumDefaultNodeUri)
?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.ethereum);
}
Node getMoneroDefaultNode({required Box<Node> nodes}) { Node getMoneroDefaultNode({required Box<Node> nodes}) {
final timeZone = DateTime.now().timeZoneOffset.inHours; final timeZone = DateTime.now().timeZoneOffset.inHours;
var nodeUri = ''; var nodeUri = '';
@ -424,6 +437,8 @@ Future<void> checkCurrentNodes(
.getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey);
final currentHavenNodeId = sharedPreferences final currentHavenNodeId = sharedPreferences
.getInt(PreferencesKey.currentHavenNodeIdKey); .getInt(PreferencesKey.currentHavenNodeIdKey);
final currentEthereumNodeId = sharedPreferences
.getInt(PreferencesKey.currentEthereumNodeIdKey);
final currentMoneroNode = nodeSource.values.firstWhereOrNull( final currentMoneroNode = nodeSource.values.firstWhereOrNull(
(node) => node.key == currentMoneroNodeId); (node) => node.key == currentMoneroNodeId);
final currentBitcoinElectrumServer = nodeSource.values.firstWhereOrNull( final currentBitcoinElectrumServer = nodeSource.values.firstWhereOrNull(
@ -432,6 +447,8 @@ Future<void> checkCurrentNodes(
(node) => node.key == currentLitecoinElectrumSeverId); (node) => node.key == currentLitecoinElectrumSeverId);
final currentHavenNodeServer = nodeSource.values.firstWhereOrNull( final currentHavenNodeServer = nodeSource.values.firstWhereOrNull(
(node) => node.key == currentHavenNodeId); (node) => node.key == currentHavenNodeId);
final currentEthereumNodeServer = nodeSource.values.firstWhereOrNull(
(node) => node.key == currentEthereumNodeId);
if (currentMoneroNode == null) { if (currentMoneroNode == null) {
final newCakeWalletNode = final newCakeWalletNode =
@ -465,6 +482,13 @@ Future<void> checkCurrentNodes(
await sharedPreferences.setInt( await sharedPreferences.setInt(
PreferencesKey.currentHavenNodeIdKey, node.key as int); 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<void> resetBitcoinElectrumServer( Future<void> resetBitcoinElectrumServer(
@ -501,3 +525,21 @@ Future<void> changeDefaultHavenNode(
await node.save(); await node.save();
}); });
} }
Future<void> addEthereumNodeList({required Box<Node> 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<void> changeEthereumCurrentNodeToDefault(
{required SharedPreferences sharedPreferences,
required Box<Node> nodes}) async {
final node = getEthereumDefaultNode(nodes: nodes);
final nodeId = node?.key as int? ?? 0;
await sharedPreferences.setInt(PreferencesKey.currentEthereumNodeIdKey, nodeId);
}

View file

@ -70,6 +70,22 @@ Future<List<Node>> loadDefaultHavenNodes() async {
return nodes; return nodes;
} }
Future<List<Node>> loadDefaultEthereumNodes() async {
final nodesRaw = await rootBundle.loadString('assets/ethereum_server_list.yml');
final loadedNodes = loadYaml(nodesRaw) as YamlList;
final nodes = <Node>[];
for (final raw in loadedNodes) {
if (raw is Map) {
final node = Node.fromMap(Map<String, Object>.from(raw));
node.type = WalletType.ethereum;
nodes.add(node);
}
}
return nodes;
}
Future resetToDefault(Box<Node> nodeSource) async { Future resetToDefault(Box<Node> nodeSource) async {
final moneroNodes = await loadDefaultNodes(); final moneroNodes = await loadDefaultNodes();
final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); final bitcoinElectrumServerList = await loadBitcoinElectrumServerList();

View file

@ -5,6 +5,7 @@ class PreferencesKey {
static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc'; static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc';
static const currentLitecoinElectrumSererIdKey = 'current_node_id_ltc'; static const currentLitecoinElectrumSererIdKey = 'current_node_id_ltc';
static const currentHavenNodeIdKey = 'current_node_id_xhv'; static const currentHavenNodeIdKey = 'current_node_id_xhv';
static const currentEthereumNodeIdKey = 'current_node_id_eth';
static const currentFiatCurrencyKey = 'current_fiat_currency'; static const currentFiatCurrencyKey = 'current_fiat_currency';
static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority';
static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode';

View file

@ -30,7 +30,8 @@ class MenuWidgetState extends State<MenuWidget> {
this.moneroIcon = Image.asset('assets/images/monero_menu.png'), this.moneroIcon = Image.asset('assets/images/monero_menu.png'),
this.bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png'), this.bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png'),
this.litecoinIcon = Image.asset('assets/images/litecoin_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; final largeScreen = 731;
@ -47,6 +48,7 @@ class MenuWidgetState extends State<MenuWidget> {
Image bitcoinIcon; Image bitcoinIcon;
Image litecoinIcon; Image litecoinIcon;
Image havenIcon; Image havenIcon;
Image ethereumIcon;
@override @override
void initState() { void initState() {
@ -92,8 +94,6 @@ class MenuWidgetState extends State<MenuWidget> {
color: Theme.of(context).accentTextTheme!.overline!.decorationColor!); color: Theme.of(context).accentTextTheme!.overline!.decorationColor!);
bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png', bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png',
color: Theme.of(context).accentTextTheme!.overline!.decorationColor!); 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( return Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
@ -259,6 +259,8 @@ class MenuWidgetState extends State<MenuWidget> {
return litecoinIcon; return litecoinIcon;
case WalletType.haven: case WalletType.haven:
return havenIcon; return havenIcon;
case WalletType.ethereum:
return ethereumIcon;
default: default:
throw Exception('No icon for ${type.toString()}'); throw Exception('No icon for ${type.toString()}');
} }

View file

@ -318,10 +318,13 @@ abstract class SettingsStoreBase with Store {
.getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey);
final havenNodeId = sharedPreferences final havenNodeId = sharedPreferences
.getInt(PreferencesKey.currentHavenNodeIdKey); .getInt(PreferencesKey.currentHavenNodeIdKey);
final ethereumNodeId = sharedPreferences
.getInt(PreferencesKey.currentEthereumNodeIdKey);
final moneroNode = nodeSource.get(nodeId); final moneroNode = nodeSource.get(nodeId);
final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId);
final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId);
final havenNode = nodeSource.get(havenNodeId); final havenNode = nodeSource.get(havenNodeId);
final ethereumNode = nodeSource.get(ethereumNodeId);
final packageInfo = await PackageInfo.fromPlatform(); final packageInfo = await PackageInfo.fromPlatform();
final shouldShowYatPopup = final shouldShowYatPopup =
sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true; sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true;
@ -344,6 +347,10 @@ abstract class SettingsStoreBase with Store {
nodes[WalletType.haven] = havenNode; nodes[WalletType.haven] = havenNode;
} }
if (ethereumNode != null) {
nodes[WalletType.ethereum] = ethereumNode;
}
return SettingsStore( return SettingsStore(
sharedPreferences: sharedPreferences, sharedPreferences: sharedPreferences,
nodes: nodes, nodes: nodes,
@ -429,10 +436,13 @@ abstract class SettingsStoreBase with Store {
.getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey);
final havenNodeId = sharedPreferences final havenNodeId = sharedPreferences
.getInt(PreferencesKey.currentHavenNodeIdKey); .getInt(PreferencesKey.currentHavenNodeIdKey);
final ethereumNodeId = sharedPreferences
.getInt(PreferencesKey.currentEthereumNodeIdKey);
final moneroNode = nodeSource.get(nodeId); final moneroNode = nodeSource.get(nodeId);
final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId);
final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId);
final havenNode = nodeSource.get(havenNodeId); final havenNode = nodeSource.get(havenNodeId);
final ethereumNode = nodeSource.get(ethereumNodeId);
if (moneroNode != null) { if (moneroNode != null) {
nodes[WalletType.monero] = moneroNode; nodes[WalletType.monero] = moneroNode;
@ -449,6 +459,10 @@ abstract class SettingsStoreBase with Store {
if (havenNode != null) { if (havenNode != null) {
nodes[WalletType.haven] = havenNode; nodes[WalletType.haven] = havenNode;
} }
if (ethereumNode != null) {
nodes[WalletType.ethereum] = ethereumNode;
}
} }
Future<void> _saveCurrentNode(Node node, WalletType walletType) async { Future<void> _saveCurrentNode(Node node, WalletType walletType) async {
@ -469,6 +483,10 @@ abstract class SettingsStoreBase with Store {
await _sharedPreferences.setInt( await _sharedPreferences.setInt(
PreferencesKey.currentHavenNodeIdKey, node.key as int); PreferencesKey.currentHavenNodeIdKey, node.key as int);
break; break;
case WalletType.ethereum:
await _sharedPreferences.setInt(
PreferencesKey.currentEthereumNodeIdKey, node.key as int);
break;
default: default:
break; break;
} }