dev
dart 658 lines 19.4 KB
Raw
1 import 'dart:async';
2 import 'dart:convert';
3 import 'dart:developer';
4
5 import 'package:bip39/bip39.dart' as bip39;
6 import 'package:blockchain_utils/blockchain_utils.dart';
7 import 'package:cw_core/amount/money.dart';
8 import 'package:cw_core/crypto_currency.dart';
9 import 'package:cw_core/encryption_file_utils.dart';
10 import 'package:cw_core/node.dart';
11 import 'package:cw_core/pathForWallet.dart';
12 import 'package:cw_core/pending_transaction.dart';
13 import 'package:cw_core/sync_status.dart';
14 import 'package:cw_core/transaction_direction.dart';
15 import 'package:cw_core/transaction_priority.dart';
16 import 'package:cw_core/wallet_addresses.dart';
17 import 'package:cw_core/wallet_base.dart';
18 import 'package:cw_core/wallet_info.dart';
19 import 'package:cw_core/wallet_keys_file.dart';
20 import 'package:cw_core/wallet_type.dart';
21 import 'package:cw_tron/default_tron_tokens.dart';
22 import 'package:cw_tron/tron_abi.dart';
23 import 'package:cw_tron/tron_balance.dart';
24 import 'package:cw_tron/tron_client.dart';
25 import 'package:cw_tron/tron_exception.dart';
26 import 'package:cw_core/tron_token.dart';
27 import 'package:cw_tron/tron_transaction_credentials.dart';
28 import 'package:cw_tron/tron_transaction_history.dart';
29 import 'package:cw_tron/tron_transaction_info.dart';
30 import 'package:cw_tron/tron_wallet_addresses.dart';
31 import 'package:mobx/mobx.dart';
32 import 'package:on_chain/on_chain.dart';
33
34 part 'tron_wallet.g.dart';
35
36 class TronWallet = TronWalletBase with _$TronWallet;
37
38 abstract class TronWalletBase
39 extends WalletBase<TronBalance, TronTransactionHistory, TronTransactionInfo>
40 with Store, WalletKeysFile {
41 TronWalletBase({
42 required WalletInfo walletInfo,
43 required DerivationInfo derivationInfo,
44 String? mnemonic,
45 String? privateKey,
46 required String password,
47 TronBalance? initialBalance,
48 required this.encryptionFileUtils,
49 this.passphrase,
50 }) : syncStatus = const NotConnectedSyncStatus(),
51 _password = password,
52 _mnemonic = mnemonic,
53 _hexPrivateKey = privateKey,
54 _client = TronClient(),
55 walletAddresses = TronWalletAddresses(walletInfo),
56 balance = ObservableMap<CryptoCurrency, TronBalance>.of(
57 {CryptoCurrency.trx: initialBalance ?? TronBalance(Money.zero(CryptoCurrency.trx))},
58 ),
59 super(walletInfo, derivationInfo) {
60 this.walletInfo = walletInfo;
61 transactionHistory = TronTransactionHistory(
62 walletInfo: walletInfo, password: password, encryptionFileUtils: encryptionFileUtils);
63 }
64
65 final String? _mnemonic;
66 final String? _hexPrivateKey;
67 final String _password;
68 final EncryptionFileUtils encryptionFileUtils;
69
70 List<TronToken> _tronTokens = [];
71
72 late final TronPrivateKey _tronPrivateKey;
73
74 late final TronPublicKey _tronPublicKey;
75
76 TronPublicKey get tronPublicKey => _tronPublicKey;
77
78 TronPrivateKey get tronPrivateKey => _tronPrivateKey;
79
80 late String _tronAddress;
81
82 late final TronClient _client;
83
84 Timer? _transactionsUpdateTimer;
85
86 @override
87 WalletAddresses walletAddresses;
88
89 @observable
90 Money? nativeTxEstimatedFee;
91
92 @observable
93 Money? trc20EstimatedFee;
94
95 @override
96 @observable
97 SyncStatus syncStatus;
98
99 @override
100 @observable
101 late ObservableMap<CryptoCurrency, TronBalance> balance;
102
103 Future<void> init() async {
104 await initTronTokens();
105
106 await walletAddresses.init();
107 await transactionHistory.init();
108 _tronPrivateKey = await getPrivateKey(
109 mnemonic: _mnemonic,
110 privateKey: _hexPrivateKey,
111 password: _password,
112 passphrase: passphrase,
113 );
114
115 _tronPublicKey = _tronPrivateKey.publicKey();
116
117 _tronAddress = _tronPublicKey.toAddress().toString();
118
119 walletAddresses.address = _tronAddress;
120
121 await save();
122 }
123
124 static Future<TronWallet> open({
125 required String name,
126 required String password,
127 required WalletInfo walletInfo,
128 required EncryptionFileUtils encryptionFileUtils,
129 }) async {
130 final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type);
131 final path = await pathForWallet(name: name, type: walletInfo.type);
132
133 Map<String, dynamic>? data;
134 try {
135 final jsonSource = await encryptionFileUtils.read(path: path, password: password);
136
137 data = json.decode(jsonSource) as Map<String, dynamic>;
138 } catch (e) {
139 if (!hasKeysFile) rethrow;
140 }
141
142 final balance = TronBalance.fromJSON(data?['balance'] as String?, CryptoCurrency.trx) ??
143 TronBalance(Money.zero(CryptoCurrency.trx));
144
145 final WalletKeysData keysData;
146 // Migrate wallet from the old scheme to then new .keys file scheme
147 if (!hasKeysFile) {
148 final mnemonic = data!['mnemonic'] as String?;
149 final privateKey = data['private_key'] as String?;
150 final passphrase = data['passphrase'] as String?;
151
152 keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey, passphrase: passphrase);
153 } else {
154 keysData = await WalletKeysFile.readKeysFile(
155 name,
156 walletInfo.type,
157 password,
158 encryptionFileUtils,
159 );
160 }
161
162 final derivationInfo = await walletInfo.getDerivationInfo();
163
164 return TronWallet(
165 walletInfo: walletInfo,
166 derivationInfo: derivationInfo,
167 password: password,
168 mnemonic: keysData.mnemonic,
169 privateKey: keysData.privateKey,
170 passphrase: keysData.passphrase,
171 initialBalance: balance,
172 encryptionFileUtils: encryptionFileUtils,
173 );
174 }
175
176 Future<void> addInitialTokens() async {
177 final initialTronTokens = DefaultTronTokens().initialTronTokens;
178
179 for (var token in initialTronTokens) {
180 final existingToken = _findCachedToken(token.contractAddress);
181
182 final newToken = TronToken.copyWith(
183 token,
184 enabled: existingToken?.enabled ?? token.enabled,
185 walletName: walletInfo.name,
186 );
187
188 await newToken.save();
189 _upsertCachedToken(newToken);
190 }
191 }
192
193 Future<void> initTronTokens() async {
194 _tronTokens = await TronToken.getAllForWallet(walletInfo.name);
195 }
196
197 TronToken? _findCachedToken(String contractAddress) {
198 for (final token in _tronTokens) {
199 if (token.contractAddress == contractAddress) return token;
200 }
201
202 return null;
203 }
204
205 void _upsertCachedToken(TronToken token) {
206 _tronTokens.removeWhere((t) => t.contractAddress == token.contractAddress);
207 _tronTokens.add(token);
208 }
209
210 String idFor(String name, WalletType type) => '${walletTypeToString(type).toLowerCase()}_$name';
211
212 Future<TronPrivateKey> getPrivateKey({
213 String? mnemonic,
214 String? privateKey,
215 required String password,
216 String? passphrase,
217 }) async {
218 assert(mnemonic != null || privateKey != null);
219
220 if (privateKey != null) return TronPrivateKey(privateKey);
221
222 final seed = bip39.mnemonicToSeed(mnemonic!, passphrase: passphrase ?? '');
223
224 // Derive a TRON private key from the seed
225 final bip44 = Bip44.fromSeed(seed, Bip44Coins.tron);
226
227 final childKey = bip44.deriveDefaultPath;
228
229 return TronPrivateKey.fromBytes(childKey.privateKey.raw);
230 }
231
232 @override
233 int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0;
234
235 @override
236 Future<void> changePassword(String password) => throw UnimplementedError("changePassword");
237
238 @override
239 Future<void> close({bool shouldCleanup = false}) async => _transactionsUpdateTimer?.cancel();
240
241 @action
242 @override
243 Future<void> connectToNode({required Node node}) async {
244 try {
245 syncStatus = ConnectingSyncStatus();
246
247 final isConnected = _client.connect(node);
248
249 if (!isConnected) {
250 throw Exception("${walletInfo.type.name.toUpperCase()} Node connection failed");
251 }
252
253 _getEstimatedFees();
254 _setTransactionUpdateTimer();
255
256 syncStatus = ConnectedSyncStatus();
257 } catch (e) {
258 syncStatus = FailedSyncStatus();
259 }
260 }
261
262 Future<void> _getEstimatedFees() async {
263 final nativeFee = await _getNativeTxFee();
264 nativeTxEstimatedFee = Money.fromInt(nativeFee, currency);
265
266 final trc20Fee = await _getTrc20TxFee();
267 trc20EstimatedFee = Money.fromInt(trc20Fee, currency);
268
269 log('Native Estimated Fee: $nativeTxEstimatedFee');
270 log('TRC20 Estimated Fee: $trc20EstimatedFee');
271 }
272
273 Future<int> _getNativeTxFee() async {
274 try {
275 final fee = await _client.getEstimatedFee(_tronPublicKey.toAddress());
276 return fee;
277 } catch (e) {
278 log(e.toString());
279 return 0;
280 }
281 }
282
283 Future<int> _getTrc20TxFee() async {
284 try {
285 final trc20fee = await _client.getTRCEstimatedFee(_tronPublicKey.toAddress());
286 return trc20fee;
287 } catch (e) {
288 log(e.toString());
289 return 0;
290 }
291 }
292
293 @action
294 @override
295 Future<void> startSync() async {
296 try {
297 syncStatus = AttemptingSyncStatus();
298
299 // Verify node health before attempting to sync
300 final isHealthy = await checkNodeHealth();
301 if (!isHealthy) {
302 syncStatus = FailedSyncStatus();
303 return;
304 }
305 await _updateBalance();
306 await fetchTransactions();
307 fetchTrc20ExcludedTransactions();
308
309 syncStatus = SyncedSyncStatus();
310 } catch (e) {
311 syncStatus = FailedSyncStatus();
312 }
313 }
314
315 @override
316 Future<PendingTransaction> createTransaction(Object credentials) async {
317 final tronCredentials = credentials as TronTransactionCredentials;
318
319 final outputs = tronCredentials.outputs;
320
321 final hasMultiDestination = outputs.length > 1;
322
323 final transactionCurrency = balance.keys.firstWhere(
324 (currency) =>
325 currency.title == tronCredentials.currency.title &&
326 currency.tag == tronCredentials.currency.tag,
327 orElse: () => throw Exception(
328 'Currency ${tronCredentials.currency.title} ${tronCredentials.currency.tag} is not accessible in the wallet, try to enable it first.'));
329
330 final walletBalanceForCurrency = balance[transactionCurrency]!.balance;
331
332 var totalAmount = Money.zero(transactionCurrency);
333 var shouldSendAll = false;
334 if (hasMultiDestination) {
335 // Tron does not have multi Destination right now
336 throw TronTransactionCreationException(transactionCurrency);
337 } else {
338 final output = outputs.first;
339
340 shouldSendAll = output.sendAll;
341
342 if (shouldSendAll) {
343 totalAmount = walletBalanceForCurrency;
344 } else {
345 totalAmount = output.cryptoAmount.copyWith(currency: transactionCurrency);
346 }
347
348 if (walletBalanceForCurrency < totalAmount || totalAmount < Money.zero(transactionCurrency)) {
349 throw TronTransactionCreationException(transactionCurrency);
350 }
351 }
352
353 final tronBalance = balance[CryptoCurrency.trx]?.balance ?? Money.zero(CryptoCurrency.trx);
354
355 final pendingTransaction = await _client.signTransaction(
356 ownerPrivKey: _tronPrivateKey,
357 toAddress: tronCredentials.outputs.first.isParsedAddress
358 ? tronCredentials.outputs.first.extractedAddress!
359 : tronCredentials.outputs.first.address,
360 amount: totalAmount,
361 tronBalance: tronBalance.amount,
362 sendAll: shouldSendAll,
363 );
364
365 return pendingTransaction;
366 }
367
368 @override
369 Future<void> updateTransactionsHistory() async {
370 await Future.wait([
371 fetchTransactions(),
372 fetchTrc20ExcludedTransactions(),
373 ]);
374 }
375
376 @override
377 Future<Map<String, TronTransactionInfo>> fetchTransactions() async {
378 final address = _tronAddress;
379
380 final transactions = await _client.fetchTransactions(address);
381
382 final Map<String, TronTransactionInfo> result = {};
383
384 final contract = ContractABI.fromJson(trc20Abi, isTron: true);
385
386 final ownerAddress = TronAddress(_tronAddress);
387
388 for (var transactionModel in transactions) {
389 if (transactionModel.isError) {
390 continue;
391 }
392
393 // Filter out spam transactions that involve receiving TRC10 assets transaction, we deal with TRX and TRC20 transactions
394 if (transactionModel.contracts?.first.type == "TransferAssetContract") {
395 continue;
396 }
397
398 var txCurrency = currency;
399 if (transactionModel.contractAddress != null) {
400 final tokenAddress = TronAddress(transactionModel.contractAddress!);
401
402 final tokenSymbol = (await _client.getTokenDetail(
403 contract,
404 "symbol",
405 ownerAddress,
406 tokenAddress,
407 ) as String?) ??
408 '';
409
410 final decimals = (await _client.getTokenDetail(
411 contract,
412 "decimals",
413 ownerAddress,
414 tokenAddress,
415 ) as BigInt?)
416 ?.toInt() ??
417 txCurrency.decimals;
418
419 txCurrency = CryptoCurrency(name: tokenSymbol, title: tokenSymbol, decimals: decimals);
420 }
421
422 result[transactionModel.hash] = TronTransactionInfo(
423 id: transactionModel.hash,
424 amount: Money(transactionModel.amount ?? BigInt.zero, txCurrency),
425 direction: TronAddress(transactionModel.from!, visible: false).toAddress() == address
426 ? TransactionDirection.outgoing
427 : TransactionDirection.incoming,
428 blockTime: transactionModel.date,
429 fee: transactionModel.fee != null ? Money.fromInt(transactionModel.fee!, currency) : null,
430 to: transactionModel.to,
431 from: transactionModel.from,
432 isPending: false,
433 );
434 }
435
436 transactionHistory.addMany(result);
437
438 await transactionHistory.save();
439
440 return transactionHistory.transactions;
441 }
442
443 Future<void> fetchTrc20ExcludedTransactions() async {
444 final address = _tronAddress;
445
446 final transactions = await _client.fetchTrc20ExcludedTransactions(address);
447
448 final Map<String, TronTransactionInfo> result = {};
449
450 for (final transactionModel in transactions) {
451 if (transactionHistory.transactions.containsKey(transactionModel.hash)) {
452 continue;
453 }
454
455 result[transactionModel.hash] = TronTransactionInfo(
456 id: transactionModel.hash,
457 amount: Money(transactionModel.amount ?? BigInt.zero, transactionModel.currency),
458 direction: transactionModel.from! == address
459 ? TransactionDirection.outgoing
460 : TransactionDirection.incoming,
461 blockTime: transactionModel.date,
462 fee: transactionModel.fee != null ? Money.fromInt(transactionModel.fee!, currency) : null,
463 to: transactionModel.to,
464 from: transactionModel.from,
465 isPending: false,
466 );
467 }
468
469 transactionHistory.addMany(result);
470
471 await transactionHistory.save();
472 }
473
474 @override
475 Object get keys => throw UnimplementedError("keys");
476
477 @override
478 Future<void> rescan({required int height}) => throw UnimplementedError("rescan");
479
480 @override
481 Future<void> save() async {
482 if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) {
483 await saveKeysFile(_password, encryptionFileUtils);
484 saveKeysFile(_password, encryptionFileUtils, true);
485 }
486
487 await walletAddresses.updateAddressesInBox();
488 final path = await makePath();
489 await encryptionFileUtils.write(path: path, password: _password, data: toJSON());
490 await transactionHistory.save();
491 }
492
493 @override
494 String? get seed => _mnemonic;
495
496 @override
497 String get privateKey => _tronPrivateKey.toHex();
498
499 @override
500 WalletKeysData get walletKeysData => WalletKeysData(
501 mnemonic: _mnemonic,
502 privateKey: privateKey,
503 passphrase: passphrase,
504 );
505
506 String toJSON() => json.encode({
507 'mnemonic': _mnemonic,
508 'private_key': privateKey,
509 'balance': balance[currency]!.toJSON(),
510 'passphrase': passphrase,
511 });
512
513 Future<void> _updateBalance() async {
514 balance[currency] = await _fetchTronBalance();
515
516 await _fetchTronTokenBalances();
517 await save();
518 }
519
520 Future<TronBalance> _fetchTronBalance() async {
521 final balance = await _client.getBalance(_tronPublicKey.toAddress());
522 return TronBalance(Money(balance, CryptoCurrency.trx));
523 }
524
525 Future<void> _fetchTronTokenBalances() async {
526 for (var token in _tronTokens.toList()) {
527 try {
528 if (token.enabled) {
529 balance[token] = await _client.fetchTronTokenBalances(
530 _tronAddress,
531 token.contractAddress,
532 currency: token,
533 );
534 } else {
535 balance.remove(token);
536 }
537 } catch (_) {}
538 }
539 }
540
541 @override
542 Future<void>? updateBalance() async => await _updateBalance();
543
544 @override
545 Future<bool> checkNodeHealth() async {
546 try {
547 // Check native balance
548 await _client.getBalance(_tronPublicKey.toAddress(), throwOnError: true);
549
550 // Check USDT token balance
551 final usdtContractAddress = DefaultTronTokens().usdt.contractAddress;
552 await _client.fetchTronTokenBalances(_tronAddress, usdtContractAddress,
553 throwOnError: true, currency: CryptoCurrency.usdttrc20);
554
555 return true;
556 } catch (e) {
557 return false;
558 }
559 }
560
561 List<TronToken> get tronTokenCurrencies => _tronTokens.toList();
562
563 Future<void> addTronToken(TronToken token) async {
564 String? iconPath;
565 if ((token.iconPath == null || token.iconPath!.isEmpty) && !token.isPotentialScam) {
566 try {
567 iconPath = CryptoCurrency.all
568 .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
569 .iconPath;
570 } catch (_) {}
571 } else if (!token.isPotentialScam) {
572 iconPath = token.iconPath;
573 }
574
575 final newToken = TronToken(
576 name: token.name,
577 symbol: token.symbol,
578 contractAddress: token.contractAddress,
579 decimal: token.decimal,
580 enabled: token.enabled,
581 tag: token.tag ?? "TRX",
582 iconPath: iconPath,
583 isPotentialScam: token.isPotentialScam,
584 walletName: walletInfo.name,
585 );
586
587 await newToken.save();
588 _upsertCachedToken(newToken);
589
590 if (newToken.enabled) {
591 balance[newToken] = await _client
592 .fetchTronTokenBalances(_tronAddress, newToken.contractAddress, currency: token);
593 } else {
594 balance.remove(newToken);
595 }
596 }
597
598 Future<void> deleteTronToken(TronToken token) async {
599 await TronToken.deleteForWallet(walletInfo.name, token.contractAddress);
600 _tronTokens.removeWhere((t) => t.contractAddress == token.contractAddress);
601
602 balance.remove(token);
603 await _removeTokenTransactionsInHistory(token);
604 _updateBalance();
605 }
606
607 Future<void> _removeTokenTransactionsInHistory(TronToken token) async {
608 transactionHistory.transactions
609 .removeWhere((key, value) => value.amount.currency.symbol == token.title);
610 await transactionHistory.save();
611 }
612
613 Future<TronToken?> getTronToken(String contractAddress) async =>
614 await _client.getTronToken(contractAddress, _tronAddress);
615
616 void _setTransactionUpdateTimer() {
617 if (_transactionsUpdateTimer?.isActive ?? false) {
618 _transactionsUpdateTimer!.cancel();
619 }
620
621 _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 30), (_) async {
622 _updateBalance();
623 await fetchTransactions();
624 fetchTrc20ExcludedTransactions();
625 });
626 }
627
628 @override
629 Future<String> signMessage(String message, {String? address}) async {
630 return _tronPrivateKey.signPersonalMessage(ascii.encode(message));
631 }
632
633 @override
634 Future<bool> verifyMessage(String message, String signature, {String? address}) async {
635 if (address == null) return false;
636
637 final pubKey = TronPublicKey.fromPersonalSignature(ascii.encode(message), signature)!;
638 return pubKey.toAddress().toString() == address;
639 }
640
641 String getTronBase58AddressFromHex(String hexAddress) => TronAddress(hexAddress).toAddress();
642
643 void updateScanProviderUsageState(bool isEnabled) {
644 if (isEnabled) {
645 fetchTransactions();
646 fetchTrc20ExcludedTransactions();
647 _setTransactionUpdateTimer();
648 } else {
649 _transactionsUpdateTimer?.cancel();
650 }
651 }
652
653 @override
654 String get password => _password;
655
656 @override
657 final String? passphrase;
658 }