dev
dart 587 lines 17.8 KB
Raw
1 import 'dart:async';
2 import 'dart:convert';
3 import 'dart:io';
4
5 import 'package:bip39/bip39.dart' as bip39;
6 import 'package:cw_core/amount/money.dart';
7 import 'package:cw_core/cake_hive.dart';
8 import 'package:cw_core/crypto_currency.dart';
9 import 'package:cw_core/encryption_file_utils.dart';
10 import 'package:cw_core/n2_node.dart';
11 import 'package:cw_core/nano_account.dart';
12 import 'package:cw_core/nano_account_info_response.dart';
13 import 'package:cw_core/node.dart';
14 import 'package:cw_core/pathForWallet.dart';
15 import 'package:cw_core/pending_transaction.dart';
16 import 'package:cw_core/sync_status.dart';
17 import 'package:cw_core/transaction_direction.dart';
18 import 'package:cw_core/transaction_priority.dart';
19 import 'package:cw_core/utils/print_verbose.dart';
20 import 'package:cw_core/wallet_base.dart';
21 import 'package:cw_core/wallet_info.dart';
22 import 'package:cw_core/wallet_keys_file.dart';
23 import 'package:cw_nano/nano_balance.dart';
24 import 'package:cw_nano/nano_client.dart';
25 import 'package:cw_nano/nano_transaction_credentials.dart';
26 import 'package:cw_nano/nano_transaction_history.dart';
27 import 'package:cw_nano/nano_transaction_info.dart';
28 import 'package:cw_nano/nano_wallet_addresses.dart';
29 import 'package:cw_nano/nano_wallet_keys.dart';
30 import 'package:cw_nano/pending_nano_transaction.dart';
31 import 'package:mobx/mobx.dart';
32 import 'package:nanoutil/nanoutil.dart';
33
34 part 'nano_wallet.g.dart';
35
36 class NanoWallet = NanoWalletBase with _$NanoWallet;
37
38 abstract class NanoWalletBase
39 extends WalletBase<NanoBalance, NanoTransactionHistory, NanoTransactionInfo>
40 with Store, WalletKeysFile {
41 NanoWalletBase({
42 required WalletInfo walletInfo,
43 required DerivationInfo derivationInfo,
44 required String mnemonic,
45 required String password,
46 NanoBalance? initialBalance,
47 required EncryptionFileUtils encryptionFileUtils,
48 this.passphrase,
49 }) : syncStatus = NotConnectedSyncStatus(),
50 _password = password,
51 _mnemonic = mnemonic,
52 _derivationType = derivationInfo.derivationType!,
53 _isTransactionUpdating = false,
54 _encryptionFileUtils = encryptionFileUtils,
55 _client = NanoClient(),
56 walletAddresses = NanoWalletAddresses(walletInfo),
57 balance = ObservableMap<CryptoCurrency, NanoBalance>.of({
58 CryptoCurrency.nano: initialBalance ??
59 NanoBalance(
60 currentBalance: Money.zero(CryptoCurrency.nano),
61 receivableBalance: Money.zero(CryptoCurrency.nano),
62 )
63 }),
64 super(walletInfo, derivationInfo) {
65 this.walletInfo = walletInfo;
66 transactionHistory = NanoTransactionHistory(
67 walletInfo: walletInfo,
68 password: password,
69 encryptionFileUtils: encryptionFileUtils,
70 );
71 if (!CakeHive.isAdapterRegistered(NanoAccount.typeId)) {
72 CakeHive.registerAdapter(NanoAccountAdapter());
73 }
74 }
75
76 String _mnemonic;
77 final String _password;
78 DerivationType _derivationType;
79
80 final EncryptionFileUtils _encryptionFileUtils;
81
82 String? _privateKey;
83 String? _publicAddress;
84 String? _hexSeed;
85 Timer? _receiveTimer;
86
87 String? _representativeAddress;
88 int repScore = 100;
89
90 bool get isRepOk => repScore >= 90;
91
92 late final NanoClient _client;
93 bool _isTransactionUpdating;
94
95 @override
96 NanoWalletAddresses walletAddresses;
97
98 @override
99 @observable
100 SyncStatus syncStatus;
101
102 @override
103 @observable
104 late ObservableMap<CryptoCurrency, NanoBalance> balance;
105
106 @override
107 String get password => _password;
108
109 static const int POLL_INTERVAL_SECONDS = 10;
110
111 // initialize the different forms of private / public key we'll need:
112 Future<void> init() async {
113 if (_derivationType == DerivationType.unknown) {
114 _derivationType = DerivationType.nano;
115 }
116
117 // our "mnemonic" is actually a hex form seed:
118 if (!_mnemonic.contains(' ')) {
119 _hexSeed = _mnemonic;
120 _mnemonic = "";
121 }
122
123 if (_hexSeed == null) {
124 if (_derivationType == DerivationType.nano) {
125 _hexSeed = bip39.mnemonicToEntropy(_mnemonic).toUpperCase();
126 } else {
127 _hexSeed = await NanoDerivations.hdMnemonicListToSeed(_mnemonic.split(' '));
128 }
129 }
130
131 final String type = (_derivationType == DerivationType.nano) ? "standard" : "hd";
132 NanoDerivationType derivationType = NanoDerivations.stringToType(type);
133
134 _privateKey = await NanoDerivations.universalSeedToPrivate(
135 _hexSeed!,
136 index: 0,
137 type: derivationType,
138 );
139 _publicAddress = await NanoDerivations.universalSeedToAddress(
140 _hexSeed!,
141 index: 0,
142 type: derivationType,
143 );
144 this.walletInfo.address = _publicAddress!;
145
146 await walletAddresses.init();
147 await transactionHistory.init();
148 await save();
149 }
150
151 @override
152 int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0; // always 0 :)
153
154 @override
155 Future<void> changePassword(String password) => throw UnimplementedError("changePassword");
156
157 @override
158 Future<void> close({bool shouldCleanup = false}) async {
159 _client.stop();
160 _receiveTimer?.cancel();
161 }
162
163 @action
164 @override
165 Future<void> connectToNode({required Node node}) async {
166 try {
167 syncStatus = ConnectingSyncStatus();
168 final isConnected = _client.connect(node);
169 if (!isConnected) {
170 throw Exception("Nano Node connection failed");
171 }
172
173 try {
174 await _updateBalance();
175 await updateTransactions();
176 await _updateRep();
177 await _receiveAll();
178 } catch (e) {
179 printV(e);
180 }
181
182 syncStatus = ConnectedSyncStatus();
183 } catch (e) {
184 printV(e);
185 syncStatus = FailedSyncStatus();
186 }
187 }
188
189 @override
190 Future<void> connectToPowNode({required Node node}) async => _client.connectPow(node);
191
192 @override
193 Future<PendingTransaction> createTransaction(Object credentials) async {
194 credentials = credentials as NanoTransactionCredentials;
195
196 BigInt runningAmount = BigInt.zero;
197 await _updateBalance();
198 BigInt runningBalance = balance[currency]?.currentBalance.amount ?? BigInt.zero;
199
200 final List<Map<String, String>> blocks = [];
201 String? previousHash;
202
203 for (var txOut in credentials.outputs) {
204 late BigInt amt;
205 if (txOut.sendAll) {
206 amt = balance[currency]?.currentBalance.amount ?? BigInt.zero;
207 } else {
208 amt = txOut.cryptoAmount.amount;
209 }
210
211 if (balance[currency]?.currentBalance != null &&
212 amt > balance[currency]!.currentBalance.amount) {
213 throw Exception("Trying to send more than entire balance!");
214 }
215
216 runningBalance = runningBalance - amt;
217
218 final block = await _client.constructSendBlock(
219 amountRaw: amt.toString(),
220 destinationAddress: txOut.isParsedAddress ? txOut.extractedAddress! : txOut.address,
221 privateKey: _privateKey!,
222 balanceAfterTx: runningBalance,
223 previousHash: previousHash,
224 );
225 previousHash = NanoSignatures.computeStateHash(
226 NanoBasedCurrency.NANO,
227 block["account"]!,
228 block["previous"]!,
229 block["representative"]!,
230 BigInt.parse(block["balance"]!),
231 block["link"]!,
232 );
233
234 blocks.add(block);
235 runningAmount += amt;
236 }
237
238 try {
239 if (runningAmount > balance[currency]!.currentBalance.amount ||
240 runningBalance < BigInt.zero) {
241 throw Exception(("Trying to send more than entire balance!"));
242 }
243 } catch (e) {
244 rethrow;
245 }
246
247 return PendingNanoTransaction(
248 amount: Money(runningAmount, currency),
249 id: "",
250 nanoClient: _client,
251 blocks: blocks,
252 );
253 }
254
255 Future<void> _receiveAll() async {
256 await _updateBalance();
257 int blocksReceived = await this._client.confirmAllReceivable(
258 destinationAddress: _publicAddress!,
259 privateKey: _privateKey!,
260 );
261
262 if (blocksReceived > 0) {
263 await Future<void>.delayed(Duration(seconds: 3));
264 _updateBalance();
265 updateTransactions();
266 }
267 }
268
269 Future<void> updateTransactionsHistory() async => await updateTransactions();
270
271 Future<bool> updateTransactions() async {
272 try {
273 if (_isTransactionUpdating) {
274 return false;
275 }
276
277 _isTransactionUpdating = true;
278 final transactions = await fetchTransactions();
279 transactionHistory.addMany(transactions);
280 await transactionHistory.save();
281 _isTransactionUpdating = false;
282 return true;
283 } catch (_) {
284 _isTransactionUpdating = false;
285 return false;
286 }
287 }
288
289 @override
290 Future<Map<String, NanoTransactionInfo>> fetchTransactions() async {
291 String address = _publicAddress!;
292
293 final transactions = await _client.fetchTransactions(address);
294
295 final Map<String, NanoTransactionInfo> result = {};
296
297 for (var transactionModel in transactions) {
298 final bool isSend = transactionModel.type == "send";
299 result[transactionModel.hash] = NanoTransactionInfo(
300 id: transactionModel.hash,
301 amountRaw: Money(transactionModel.amount, currency),
302 height: transactionModel.height,
303 direction: isSend ? TransactionDirection.outgoing : TransactionDirection.incoming,
304 confirmed: transactionModel.confirmed,
305 date: transactionModel.date ?? DateTime.now(),
306 confirmations: transactionModel.confirmed ? 1 : 0,
307 to: isSend ? transactionModel.account : address,
308 from: isSend ? address : transactionModel.account,
309 );
310 }
311
312 return result;
313 }
314
315 @override
316 NanoWalletKeys get keys => NanoWalletKeys(seedKey: _hexSeed!);
317
318 @override
319 String? get privateKey => _privateKey!;
320
321 @override
322 Future<void> rescan({required int height}) async {
323 updateTransactions();
324 _updateBalance();
325 return;
326 }
327
328 @override
329 Future<void> save() async {
330 if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) {
331 await saveKeysFile(_password, _encryptionFileUtils);
332 saveKeysFile(_password, _encryptionFileUtils, true);
333 }
334
335 await walletAddresses.updateAddressesInBox();
336 final path = await makePath();
337 await _encryptionFileUtils.write(path: path, password: _password, data: toJSON());
338 await transactionHistory.save();
339 }
340
341 @override
342 String? get seed => _mnemonic.isNotEmpty ? _mnemonic : null;
343
344 String get hexSeed => _hexSeed!;
345
346 @override
347 WalletKeysData get walletKeysData => WalletKeysData(
348 mnemonic: _mnemonic,
349 altMnemonic: hexSeed,
350 passphrase: passphrase,
351 );
352
353 String get representative => _representativeAddress ?? "";
354
355 @action
356 @override
357 Future<void> startSync() async {
358 try {
359 syncStatus = AttemptingSyncStatus();
360
361 // setup a timer to receive transactions periodically:
362 _receiveTimer?.cancel();
363 _receiveTimer = Timer.periodic(const Duration(seconds: POLL_INTERVAL_SECONDS), (timer) async {
364 // get our balance:
365 await _updateBalance();
366 // if we have anything to receive, process it:
367 if (balance[currency]!.receivableBalance.amount > BigInt.zero) {
368 await _receiveAll();
369 }
370 });
371
372 // also run once, immediately:
373 await _updateBalance();
374 bool updateSuccess = await updateTransactions();
375 if (!updateSuccess) {
376 syncStatus = FailedSyncStatus();
377 return;
378 }
379
380 syncStatus = SyncedSyncStatus();
381 } catch (e) {
382 printV(e);
383 syncStatus = FailedSyncStatus();
384 rethrow;
385 }
386 }
387
388 String toJSON() => json.encode({
389 'seedKey': _hexSeed,
390 'mnemonic': _mnemonic,
391 'currentBalance': balance[currency]?.currentBalance.toString() ?? "0",
392 'receivableBalance': balance[currency]?.receivableBalance.toString() ?? "0",
393 'derivationType': _derivationType.toString()
394 });
395
396 static Future<NanoWallet> open({
397 required String name,
398 required String password,
399 required WalletInfo walletInfo,
400 required EncryptionFileUtils encryptionFileUtils,
401 }) async {
402 final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
403 final path = await pathForWallet(name: name, type: walletInfo.type);
404
405 Map<String, dynamic>? data = null;
406 try {
407 final jsonSource = await encryptionFileUtils.read(path: path, password: password);
408
409 data = json.decode(jsonSource) as Map<String, dynamic>;
410 } catch (e) {
411 if (!hasKeysFile) rethrow;
412 }
413
414 final balance = NanoBalance.fromRawString(
415 currentBalance: data?['currentBalance'] as String? ?? "0",
416 receivableBalance: data?['receivableBalance'] as String? ?? "0",
417 );
418
419 final WalletKeysData keysData;
420 // Migrate wallet from the old scheme to then new .keys file scheme
421 if (!hasKeysFile) {
422 final mnemonic = data!['mnemonic'] as String;
423 final isHexSeed = !mnemonic.contains(' ');
424
425 keysData = WalletKeysData(
426 mnemonic: isHexSeed ? null : mnemonic, altMnemonic: isHexSeed ? mnemonic : null);
427 } else {
428 keysData = await WalletKeysFile.readKeysFile(
429 name,
430 walletInfo.type,
431 password,
432 encryptionFileUtils,
433 );
434 }
435
436 DerivationType derivationType = DerivationType.nano;
437 if (data?['derivationType'] == "DerivationType.bip39") {
438 derivationType = DerivationType.bip39;
439 }
440
441 final derivationInfo = await walletInfo.getDerivationInfo();
442 derivationInfo.derivationType ??= derivationType;
443 if (derivationInfo.derivationType == DerivationType.unknown) {
444 if (data?['derivationType'] != null) {
445 derivationInfo.derivationType = derivationType;
446 } else {
447 derivationInfo.derivationType = DerivationType.bip39;
448 }
449 }
450 derivationInfo.save();
451
452 return NanoWallet(
453 walletInfo: walletInfo,
454 derivationInfo: derivationInfo,
455 password: password,
456 mnemonic: keysData.mnemonic!,
457 initialBalance: balance,
458 encryptionFileUtils: encryptionFileUtils,
459 );
460 // init() should always be run after this!
461 }
462
463 Future<void> _updateBalance() async {
464 var oldBalance = balance[currency];
465 try {
466 balance[currency] = await _client.getBalance(_publicAddress!);
467 } catch (e) {
468 printV("Failed to get balance $e");
469 // if we don't have a balance, we should at least create one, since it's a late binding
470 // otherwise, it's better to just leave it as whatever it was before:
471 if (balance[currency] == null) {
472 balance[currency] = NanoBalance(
473 currentBalance: Money.zero(currency), receivableBalance: Money.zero(currency));
474 }
475 }
476 // don't save unnecessarily:
477 // trying to save too frequently can cause problems with the file system
478 // since nano is updated frequently this can be a problem, so we only save if there is a change:
479 if (oldBalance == null ||
480 balance[currency]!.currentBalance != oldBalance.currentBalance ||
481 balance[currency]!.receivableBalance != oldBalance.receivableBalance) {
482 await save();
483 }
484 }
485
486 Future<void> _updateRep() async {
487 try {
488 AccountInfoResponse accountInfo = (await _client.getAccountInfo(_publicAddress!))!;
489 _representativeAddress = accountInfo.representative;
490 } catch (e) {
491 // account not found:
492 _representativeAddress = await _client.getRepFromPrefs();
493 throw Exception("Failed to get representative address $e");
494 }
495
496 repScore = await _client.getRepScore(_representativeAddress!);
497 }
498
499 Future<void> regenerateAddress() async {
500 final NanoDerivationType type = (_derivationType == DerivationType.nano)
501 ? NanoDerivationType.STANDARD
502 : NanoDerivationType.HD;
503 _privateKey = await NanoDerivations.universalSeedToPrivate(
504 _hexSeed!,
505 index: this.walletAddresses.account!.id,
506 type: type,
507 );
508 _publicAddress = await NanoDerivations.universalSeedToAddress(
509 _hexSeed!,
510 index: this.walletAddresses.account!.id,
511 type: type,
512 );
513
514 this.walletInfo.address = _publicAddress!;
515 this.walletAddresses.address = _publicAddress!;
516 }
517
518 Future<void> changeRep(String address) async {
519 try {
520 final String hash = await _client.changeRep(
521 privateKey: _privateKey!,
522 repAddress: address,
523 ourAddress: _publicAddress!,
524 );
525 if (hash.isNotEmpty) {
526 _representativeAddress = address;
527 }
528 } catch (e) {
529 throw Exception("Failed to change representative address $e");
530 }
531 }
532
533 Future<List<N2Node>> getN2Reps() async {
534 return _client.getN2Reps();
535 }
536
537 @override
538 Future<void>? updateBalance() async => await _updateBalance();
539
540 @override
541 Future<bool> checkNodeHealth() async {
542 try {
543 await _client.getAccountInfo(_publicAddress!, throwOnError: true);
544 return true;
545 } catch (_) {
546 return false;
547 }
548 }
549
550 @override
551 Future<void> renameWalletFiles(String newWalletName) async {
552 final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
553 final currentWalletFile = File(currentWalletPath);
554
555 final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
556 final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName');
557
558 // Copies current wallet files into new wallet name's dir and files
559 if (currentWalletFile.existsSync()) {
560 final newWalletPath = await pathForWallet(name: newWalletName, type: type);
561 await currentWalletFile.copy(newWalletPath);
562 }
563 if (currentTransactionsFile.existsSync()) {
564 final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
565 await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName');
566 }
567
568 // Delete old name's dir and files
569 await Directory(currentDirPath).delete(recursive: true);
570 }
571
572 @override
573 Future<String> signMessage(String message, {String? address = null}) async {
574 return NanoSignatures.signMessage(message, privateKey!);
575 }
576
577 @override
578 Future<bool> verifyMessage(String message, String signature, {String? address = null}) async {
579 if (address == null) {
580 return false;
581 }
582 return await NanoSignatures.verifyMessage(message, signature, address);
583 }
584
585 @override
586 final String? passphrase;
587 }