1
-import 'dart:async';
1
import 'dart:convert';
3
-import 'dart:io';
4
-import 'dart:math';
2
6
-import 'package:cw_core/crypto_currency.dart';
3
import 'package:cw_core/cake_hive.dart';
8
-import 'package:cw_core/node.dart';
4
+import 'package:cw_core/crypto_currency.dart';
5
+import 'package:cw_core/erc20_token.dart';
6
import 'package:cw_core/pathForWallet.dart';
10
-import 'package:cw_core/pending_transaction.dart';
11
-import 'package:cw_core/sync_status.dart';
7
import 'package:cw_core/transaction_direction.dart';
13
-import 'package:cw_core/transaction_priority.dart';
14
-import 'package:cw_core/wallet_addresses.dart';
15
-import 'package:cw_core/wallet_base.dart';
8
import 'package:cw_core/wallet_info.dart';
9
import 'package:cw_ethereum/default_ethereum_erc20_tokens.dart';
18
-import 'package:cw_ethereum/erc20_balance.dart';
10
import 'package:cw_ethereum/ethereum_client.dart';
20
-import 'package:cw_ethereum/ethereum_exceptions.dart';
21
-import 'package:cw_ethereum/ethereum_formatter.dart';
22
-import 'package:cw_ethereum/ethereum_transaction_credentials.dart';
11
import 'package:cw_ethereum/ethereum_transaction_history.dart';
12
import 'package:cw_ethereum/ethereum_transaction_info.dart';
25
-import 'package:cw_ethereum/ethereum_transaction_model.dart';
26
-import 'package:cw_ethereum/ethereum_transaction_priority.dart';
27
-import 'package:cw_ethereum/ethereum_wallet_addresses.dart';
28
-import 'package:cw_ethereum/file.dart';
29
-import 'package:cw_core/erc20_token.dart';
30
-import 'package:hive/hive.dart';
31
-import 'package:hex/hex.dart';
32
-import 'package:mobx/mobx.dart';
33
-import 'package:shared_preferences/shared_preferences.dart';
34
-import 'package:web3dart/crypto.dart';
35
-import 'package:web3dart/web3dart.dart';
36
-import 'package:bip39/bip39.dart' as bip39;
37
-import 'package:bip32/bip32.dart' as bip32;
38
-
39
-part 'ethereum_wallet.g.dart';
13
+import 'package:cw_evm/evm_chain_transaction_history.dart';
14
+import 'package:cw_evm/evm_chain_transaction_info.dart';
15
+import 'package:cw_evm/evm_chain_transaction_model.dart';
16
+import 'package:cw_evm/evm_chain_wallet.dart';
17
+import 'package:cw_evm/evm_erc20_balance.dart';
18
+import 'package:cw_evm/file.dart';
19
+
20
+class EthereumWallet extends EVMChainWallet {
21
+ EthereumWallet({
22
+ required super.client,
23
+ required super.password,
24
+ required super.walletInfo,
25
+ super.mnemonic,
26
+ super.initialBalance,
27
+ super.privateKey,
28
+ }) : super(nativeCurrency: CryptoCurrency.eth);
29
41
-class EthereumWallet = EthereumWalletBase with _$EthereumWallet;
42
-
43
-abstract class EthereumWalletBase
44
- extends WalletBase<ERC20Balance, EthereumTransactionHistory, EthereumTransactionInfo>
45
- with Store {
46
- EthereumWalletBase({
47
- required WalletInfo walletInfo,
48
- String? mnemonic,
49
- String? privateKey,
50
- required String password,
51
- ERC20Balance? initialBalance,
52
- }) : syncStatus = NotConnectedSyncStatus(),
53
- _password = password,
54
- _mnemonic = mnemonic,
55
- _hexPrivateKey = privateKey,
56
- _isTransactionUpdating = false,
57
- _client = EthereumClient(),
58
- walletAddresses = EthereumWalletAddresses(walletInfo),
59
- balance = ObservableMap<CryptoCurrency, ERC20Balance>.of(
60
- {CryptoCurrency.eth: initialBalance ?? ERC20Balance(BigInt.zero)}),
61
- super(walletInfo) {
62
- this.walletInfo = walletInfo;
63
- transactionHistory = EthereumTransactionHistory(walletInfo: walletInfo, password: password);
30
+ @override
31
+ void addInitialTokens() {
32
+ final initialErc20Tokens = DefaultEthereumErc20Tokens().initialErc20Tokens;
33
65
- if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) {
66
- CakeHive.registerAdapter(Erc20TokenAdapter());
34
+ for (var token in initialErc20Tokens) {
35
+ evmChainErc20TokensBox.put(token.contractAddress, token);
36
}
68
-
69
- _sharedPrefs.complete(SharedPreferences.getInstance());
37
}
38
72
- final String? _mnemonic;
73
- final String? _hexPrivateKey;
74
- final String _password;
75
-
76
- late final Box<Erc20Token> erc20TokensBox;
77
-
78
- late final Box<Erc20Token> ethereumErc20TokensBox;
79
-
80
- late final EthPrivateKey _ethPrivateKey;
81
-
82
- EthPrivateKey get ethPrivateKey => _ethPrivateKey;
83
-
84
- late EthereumClient _client;
85
-
86
- int? _gasPrice;
87
- int? _estimatedGas;
88
- bool _isTransactionUpdating;
89
-
90
- // TODO: remove after integrating our own node and having eth_newPendingTransactionFilter
91
- Timer? _transactionsUpdateTimer;
92
-
93
- @override
94
- WalletAddresses walletAddresses;
95
-
39
@override
97
- @observable
98
- SyncStatus syncStatus;
40
+ Future<bool> checkIfScanProviderIsEnabled() async {
41
+ bool isEtherscanEnabled = (await sharedPrefs.future).getBool("use_etherscan") ?? true;
42
+ return isEtherscanEnabled;
43
+ }
44
45
@override
101
- @observable
102
- late ObservableMap<CryptoCurrency, ERC20Balance> balance;
103
-
104
- Completer<SharedPreferences> _sharedPrefs = Completer();
105
-
106
- Future<void> init() async {
46
+ Future<void> initErc20TokensBox() async {
47
+ // This is for ethereum wallets,
48
+ // Other wallets would override and initialize their respective boxes with their boxNames.
49
await movePreviousErc20BoxConfigsToNewBox();
108
-
109
- await walletAddresses.init();
110
- await transactionHistory.init();
111
- _ethPrivateKey = await getPrivateKey(
112
- mnemonic: _mnemonic,
113
- privateKey: _hexPrivateKey,
114
- password: _password,
115
- );
116
- walletAddresses.address = _ethPrivateKey.address.toString();
117
- await save();
50
}
51
52
/// Majorly for backward compatibility for previous configs that have been set.
53
Future<void> movePreviousErc20BoxConfigsToNewBox() async {
54
// Opens a box specific to this wallet
123
- ethereumErc20TokensBox = await CakeHive.openBox<Erc20Token>(
55
+ evmChainErc20TokensBox = await CakeHive.openBox<Erc20Token>(
56
"${walletInfo.name.replaceAll(" ", "_")}_${Erc20Token.ethereumBoxName}");
57
58
//Open the previous token configs box
62
if (erc20TokensBox.isEmpty) {
63
// If it's empty, but the new wallet specific box is also empty,
64
// we load the initial tokens to the new box.
133
- if (ethereumErc20TokensBox.isEmpty) addInitialTokens();
65
+ if (evmChainErc20TokensBox.isEmpty) addInitialTokens();
66
return;
67
}
68
73
await erc20TokensBox.deleteFromDisk();
74
75
// Add all the previous tokens with configs to the new box
144
- ethereumErc20TokensBox.addAll(allValues);
145
- }
146
-
147
- @override
148
- int calculateEstimatedFee(TransactionPriority priority, int? amount) {
149
- try {
150
- if (priority is EthereumTransactionPriority) {
151
- final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt();
152
- return (_gasPrice! + priorityFee) * (_estimatedGas ?? 0);
153
- }
154
-
155
- return 0;
156
- } catch (e) {
157
- return 0;
158
- }
159
- }
160
-
161
- @override
162
- Future<void> changePassword(String password) {
163
- throw UnimplementedError("changePassword");
164
- }
165
-
166
- @override
167
- void close() {
168
- _client.stop();
169
- _transactionsUpdateTimer?.cancel();
170
- }
171
-
172
- @action
173
- @override
174
- Future<void> connectToNode({required Node node}) async {
175
- try {
176
- syncStatus = ConnectingSyncStatus();
177
-
178
- final isConnected = _client.connect(node);
179
-
180
- if (!isConnected) {
181
- throw Exception("Ethereum Node connection failed");
182
- }
183
-
184
- _client.setListeners(_ethPrivateKey.address, _onNewTransaction);
185
-
186
- _setTransactionUpdateTimer();
187
-
188
- syncStatus = ConnectedSyncStatus();
189
- } catch (e) {
190
- syncStatus = FailedSyncStatus();
191
- }
192
- }
193
-
194
- @override
195
- Future<PendingTransaction> createTransaction(Object credentials) async {
196
- final _credentials = credentials as EthereumTransactionCredentials;
197
- final outputs = _credentials.outputs;
198
- final hasMultiDestination = outputs.length > 1;
199
-
200
- final CryptoCurrency transactionCurrency =
201
- balance.keys.firstWhere((element) => element.title == _credentials.currency.title);
202
-
203
- final _erc20Balance = balance[transactionCurrency]!;
204
- BigInt totalAmount = BigInt.zero;
205
- int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
206
- num amountToEthereumMultiplier = pow(10, exponent);
207
-
208
- // so far this can not be made with Ethereum as Ethereum does not support multiple recipients
209
- if (hasMultiDestination) {
210
- if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
211
- throw EthereumTransactionCreationException(transactionCurrency);
212
- }
213
-
214
- final totalOriginalAmount = EthereumFormatter.parseEthereumAmountToDouble(
215
- outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
216
- totalAmount = BigInt.from(totalOriginalAmount * amountToEthereumMultiplier);
217
-
218
- if (_erc20Balance.balance < totalAmount) {
219
- throw EthereumTransactionCreationException(transactionCurrency);
220
- }
221
- } else {
222
- final output = outputs.first;
223
- // since the fees are taken from Ethereum
224
- // then no need to subtract the fees from the amount if send all
225
- final BigInt allAmount;
226
- if (transactionCurrency is Erc20Token) {
227
- allAmount = _erc20Balance.balance;
228
- } else {
229
- allAmount = _erc20Balance.balance -
230
- BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
231
- }
232
- final totalOriginalAmount =
233
- EthereumFormatter.parseEthereumAmountToDouble(output.formattedCryptoAmount ?? 0);
234
- totalAmount = output.sendAll
235
- ? allAmount
236
- : BigInt.from(totalOriginalAmount * amountToEthereumMultiplier);
237
-
238
- if (_erc20Balance.balance < totalAmount) {
239
- throw EthereumTransactionCreationException(transactionCurrency);
240
- }
241
- }
242
-
243
- final pendingEthereumTransaction = await _client.signTransaction(
244
- privateKey: _ethPrivateKey,
245
- toAddress: _credentials.outputs.first.isParsedAddress
246
- ? _credentials.outputs.first.extractedAddress!
247
- : _credentials.outputs.first.address,
248
- amount: totalAmount.toString(),
249
- gas: _estimatedGas!,
250
- priority: _credentials.priority!,
251
- currency: transactionCurrency,
252
- exponent: exponent,
253
- contractAddress:
254
- transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null,
76
+ evmChainErc20TokensBox.addAll(allValues);
77
+ }
78
+
79
+ @override
80
+ EVMChainTransactionInfo getTransactionInfo(
81
+ EVMChainTransactionModel transactionModel, String address) {
82
+ final model = EthereumTransactionInfo(
83
+ id: transactionModel.hash,
84
+ height: transactionModel.blockNumber,
85
+ ethAmount: transactionModel.amount,
86
+ direction: transactionModel.from == address
87
+ ? TransactionDirection.outgoing
88
+ : TransactionDirection.incoming,
89
+ isPending: false,
90
+ date: transactionModel.date,
91
+ confirmations: transactionModel.confirmations,
92
+ ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice,
93
+ exponent: transactionModel.tokenDecimal ?? 18,
94
+ tokenSymbol: transactionModel.tokenSymbol ?? "ETH",
95
+ to: transactionModel.to,
96
+ from: transactionModel.from,
97
);
256
-
257
- return pendingEthereumTransaction;
258
- }
259
-
260
- Future<void> _updateTransactions() async {
261
- try {
262
- if (_isTransactionUpdating) {
263
- return;
264
- }
265
- bool isEtherscanEnabled = (await _sharedPrefs.future).getBool("use_etherscan") ?? true;
266
- if (!isEtherscanEnabled) {
267
- return;
268
- }
269
-
270
- _isTransactionUpdating = true;
271
- final transactions = await fetchTransactions();
272
- transactionHistory.addMany(transactions);
273
- await transactionHistory.save();
274
- _isTransactionUpdating = false;
275
- } catch (_) {
276
- _isTransactionUpdating = false;
277
- }
98
+ return model;
99
}
100
101
@override
281
- Future<Map<String, EthereumTransactionInfo>> fetchTransactions() async {
282
- final address = _ethPrivateKey.address.hex;
283
- final transactions = await _client.fetchTransactions(address);
284
-
285
- final List<Future<List<EthereumTransactionModel>>> erc20TokensTransactions = [];
286
-
287
- for (var token in balance.keys) {
288
- if (token is Erc20Token) {
289
- erc20TokensTransactions.add(_client.fetchTransactions(
290
- address,
291
- contractAddress: token.contractAddress,
292
- ));
293
- }
294
- }
295
-
296
- final tokensTransaction = await Future.wait(erc20TokensTransactions);
297
- transactions.addAll(tokensTransaction.expand((element) => element));
298
-
299
- final Map<String, EthereumTransactionInfo> result = {};
300
-
301
- for (var transactionModel in transactions) {
302
- if (transactionModel.isError) {
303
- continue;
304
- }
305
-
306
- result[transactionModel.hash] = EthereumTransactionInfo(
307
- id: transactionModel.hash,
308
- height: transactionModel.blockNumber,
309
- ethAmount: transactionModel.amount,
310
- direction: transactionModel.from == address
311
- ? TransactionDirection.outgoing
312
- : TransactionDirection.incoming,
313
- isPending: false,
314
- date: transactionModel.date,
315
- confirmations: transactionModel.confirmations,
316
- ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice,
317
- exponent: transactionModel.tokenDecimal ?? 18,
318
- tokenSymbol: transactionModel.tokenSymbol ?? "ETH",
319
- to: transactionModel.to,
320
- );
321
- }
322
-
323
- return result;
324
- }
325
-
326
- @override
327
- Object get keys => throw UnimplementedError("keys");
102
+ String getTransactionHistoryFileName() => 'transactions.json';
103
104
@override
330
- Future<void> rescan({required int height}) {
331
- throw UnimplementedError("rescan");
332
- }
333
-
334
- @override
335
- Future<void> save() async {
336
- await walletAddresses.updateAddressesInBox();
337
- final path = await makePath();
338
- await write(path: path, password: _password, data: toJSON());
339
- await transactionHistory.save();
105
+ Erc20Token createNewErc20TokenObject(Erc20Token token, String? iconPath) {
106
+ return Erc20Token(
107
+ name: token.name,
108
+ symbol: token.symbol,
109
+ contractAddress: token.contractAddress,
110
+ decimal: token.decimal,
111
+ enabled: token.enabled,
112
+ tag: token.tag ?? "ETH",
113
+ iconPath: iconPath,
114
+ );
115
}
116
117
@override
343
- String? get seed => _mnemonic;
344
-
345
- @override
346
- String get privateKey => HEX.encode(_ethPrivateKey.privateKey);
347
-
348
- @action
349
- @override
350
- Future<void> startSync() async {
351
- try {
352
- syncStatus = AttemptingSyncStatus();
353
- await _updateBalance();
354
- await _updateTransactions();
355
- _gasPrice = await _client.getGasUnitPrice();
356
- _estimatedGas = await _client.getEstimatedGas();
357
-
358
- Timer.periodic(
359
- const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasUnitPrice());
360
- Timer.periodic(const Duration(seconds: 10),
361
- (timer) async => _estimatedGas = await _client.getEstimatedGas());
362
-
363
- syncStatus = SyncedSyncStatus();
364
- } catch (e) {
365
- syncStatus = FailedSyncStatus();
366
- }
118
+ EVMChainTransactionHistory setUpTransactionHistory(WalletInfo walletInfo, String password) {
119
+ return EthereumTransactionHistory(walletInfo: walletInfo, password: password);
120
}
121
369
- Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
370
-
371
- String toJSON() => json.encode({
372
- 'mnemonic': _mnemonic,
373
- 'private_key': privateKey,
374
- 'balance': balance[currency]!.toJSON(),
375
- });
376
-
377
- static Future<EthereumWallet> open({
378
- required String name,
379
- required String password,
380
- required WalletInfo walletInfo,
381
- }) async {
122
+ static Future<EthereumWallet> open(
123
+ {required String name, required String password, required WalletInfo walletInfo}) async {
124
final path = await pathForWallet(name: name, type: walletInfo.type);
125
final jsonSource = await read(path: path, password: password);
126
final data = json.decode(jsonSource) as Map;
127
final mnemonic = data['mnemonic'] as String?;
128
final privateKey = data['private_key'] as String?;
387
- final balance = ERC20Balance.fromJSON(data['balance'] as String) ?? ERC20Balance(BigInt.zero);
129
+ final balance = EVMChainERC20Balance.fromJSON(data['balance'] as String) ??
130
+ EVMChainERC20Balance(BigInt.zero);
131
132
return EthereumWallet(
133
walletInfo: walletInfo,
135
mnemonic: mnemonic,
136
privateKey: privateKey,
137
initialBalance: balance,
138
+ client: EthereumClient(),
139
);
140
}
397
-
398
- Future<void> _updateBalance() async {
399
- balance[currency] = await _fetchEthBalance();
400
-
401
- await _fetchErc20Balances();
402
- await save();
403
- }
404
-
405
- Future<ERC20Balance> _fetchEthBalance() async {
406
- final balance = await _client.getBalance(_ethPrivateKey.address);
407
- return ERC20Balance(balance.getInWei);
408
- }
409
-
410
- Future<void> _fetchErc20Balances() async {
411
- for (var token in ethereumErc20TokensBox.values) {
412
- try {
413
- if (token.enabled) {
414
- balance[token] = await _client.fetchERC20Balances(
415
- _ethPrivateKey.address,
416
- token.contractAddress,
417
- );
418
- } else {
419
- balance.remove(token);
420
- }
421
- } catch (_) {}
422
- }
423
- }
424
-
425
- Future<EthPrivateKey> getPrivateKey(
426
- {String? mnemonic, String? privateKey, required String password}) async {
427
- assert(mnemonic != null || privateKey != null);
428
-
429
- if (privateKey != null) {
430
- return EthPrivateKey.fromHex(privateKey);
431
- }
432
-
433
- final seed = bip39.mnemonicToSeed(mnemonic!);
434
-
435
- final root = bip32.BIP32.fromSeed(seed);
436
-
437
- const _hdPathEthereum = "m/44'/60'/0'/0";
438
- const index = 0;
439
- final addressAtIndex = root.derivePath("$_hdPathEthereum/$index");
440
-
441
- return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List<int>));
442
- }
443
-
444
- Future<void>? updateBalance() async => await _updateBalance();
445
-
446
- List<Erc20Token> get erc20Currencies => ethereumErc20TokensBox.values.toList();
447
-
448
- Future<void> addErc20Token(Erc20Token token) async {
449
- String? iconPath;
450
- try {
451
- iconPath = CryptoCurrency.all
452
- .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
453
- .iconPath;
454
- } catch (_) {}
455
-
456
- final _token = Erc20Token(
457
- name: token.name,
458
- symbol: token.symbol,
459
- contractAddress: token.contractAddress,
460
- decimal: token.decimal,
461
- enabled: token.enabled,
462
- tag: token.tag ?? "ETH",
463
- iconPath: iconPath,
464
- );
465
-
466
- await ethereumErc20TokensBox.put(_token.contractAddress, _token);
467
-
468
- if (_token.enabled) {
469
- balance[_token] = await _client.fetchERC20Balances(
470
- _ethPrivateKey.address,
471
- _token.contractAddress,
472
- );
473
- } else {
474
- balance.remove(_token);
475
- }
476
- }
477
-
478
- Future<void> deleteErc20Token(Erc20Token token) async {
479
- await token.delete();
480
-
481
- balance.remove(token);
482
- _updateBalance();
483
- }
484
-
485
- Future<Erc20Token?> getErc20Token(String contractAddress) async =>
486
- await _client.getErc20Token(contractAddress);
487
-
488
- void _onNewTransaction() {
489
- _updateBalance();
490
- _updateTransactions();
491
- }
492
-
493
- void addInitialTokens() {
494
- final initialErc20Tokens = DefaultErc20Tokens().initialErc20Tokens;
495
-
496
- initialErc20Tokens.forEach((token) => ethereumErc20TokensBox.put(token.contractAddress, token));
497
- }
498
-
499
- @override
500
- Future<void> renameWalletFiles(String newWalletName) async {
501
- final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
502
- final currentWalletFile = File(currentWalletPath);
503
-
504
- final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
505
- final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName');
506
-
507
- // Copies current wallet files into new wallet name's dir and files
508
- if (currentWalletFile.existsSync()) {
509
- final newWalletPath = await pathForWallet(name: newWalletName, type: type);
510
- await currentWalletFile.copy(newWalletPath);
511
- }
512
- if (currentTransactionsFile.existsSync()) {
513
- final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
514
- await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName');
515
- }
516
-
517
- // Delete old name's dir and files
518
- await Directory(currentDirPath).delete(recursive: true);
519
- }
520
-
521
- void _setTransactionUpdateTimer() {
522
- if (_transactionsUpdateTimer?.isActive ?? false) {
523
- _transactionsUpdateTimer!.cancel();
524
- }
525
-
526
- _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 10), (_) {
527
- _updateTransactions();
528
- _updateBalance();
529
- });
530
- }
531
-
532
- void updateEtherscanUsageState(bool isEnabled) {
533
- if (isEnabled) {
534
- _updateTransactions();
535
- _setTransactionUpdateTimer();
536
- } else {
537
- _transactionsUpdateTimer?.cancel();
538
- }
539
- }
540
-
541
- @override
542
- String signMessage(String message, {String? address}) =>
543
- bytesToHex(_ethPrivateKey.signPersonalMessageToUint8List(ascii.encode(message)));
544
-
545
- Web3Client? getWeb3Client() => _client.getWeb3Client();
141
}