CW-525-Add-Tron-Wallet (#1327)

* chore: Initial setup for Tron Wallet * feat: Create Tron Wallet base flow implemented, keys, address, receive, restore and proxy classes all setup * feat: Display seed and key within the app * feat: Activate restore from key and seed for Tron wallet * feat: Add icon for tron wallet in wallet listing page * feat: Activate display of receive address for tron * feat: Fetch and display tron balance, sending transaction flow setup, fee limit calculation setup * feat: Implement sending of native tron, setup sending of trc20 tokens * chore: Rename function * Delete lib/tron/tron.dart * feat: Activate exchange for tron and its tokens, implement balance display for trc20 tokens and setup secrets configuration for tron * feat: Implement tron token management, add, remove, delete, and get tokens in home settings view, also minor cleanup * feat: Activate buy and sell for tron * feat: Implement restore from QR, transactions history listing for both native transactions and trc20 transactions * feat: Activate send all and do some minor cleanups * chore: Fix some lint infos and warnings * chore: Adjust configurations * ci: Modify CI to create and add secrets for node * fix: Fixes made while self reviewing the PR for this feature * feat: Add guide for adding new wallet types, and add fixes to requested changes * fix: Handle exceptions gracefully * fix: Alternative for trc20 estimated fee * fix: Fixes to display of amount and fee, removing clashes * fix: Fee calculation WIP * fix: Fix issue with handling of send all flow and display of amount and fee values before broadcasting transaction * fix: PR review fixes and fix merge conflicts * fix: Modify fetching assetOfTransaction [skip ci] * fix: Move tron settings migration to 33

Adegoke David committed May 3, 2024 at 19:00 UTC d1870ba8b87dbe918c0667f588f4376a802a8406
82 files changed +3660 -62
.github/workflows/pr_test_build.yml
+2
@@ -113,6 +113,7 @@ jobs:
113 touch lib/.secrets.g.dart
114 touch cw_evm/lib/.secrets.g.dart
115 touch cw_solana/lib/.secrets.g.dart
116 + touch cw_tron/lib/.secrets.g.dart
117 echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart
118 echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart
119 echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart
@@ -150,6 +151,7 @@ jobs:
151 echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart
152 echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
153 echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
154 + echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart
155
156 - name: Rename app
157 run: |
.gitignore
+3
@@ -94,9 +94,11 @@ android/app/key.jks
94 **/tool/.evm-secrets-config.json
95 **/tool/.ethereum-secrets-config.json
96 **/tool/.solana-secrets-config.json
97 +**/tool/.tron-secrets-config.json
98 **/lib/.secrets.g.dart
99 **/cw_evm/lib/.secrets.g.dart
100 **/cw_solana/lib/.secrets.g.dart
101 +**/cw_tron/lib/.secrets.g.dart
102
103 vendor/
104
@@ -132,6 +134,7 @@ lib/bitcoin_cash/bitcoin_cash.dart
134 lib/nano/nano.dart
135 lib/polygon/polygon.dart
136 lib/solana/solana.dart
137 +lib/tron/tron.dart
138
139 ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png
140 ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png
android/app/src/main/AndroidManifestBase.xml
+3
@@ -67,6 +67,9 @@
67 <data android:scheme="polygon-wallet" />
68 <data android:scheme="polygon_wallet" />
69 <data android:scheme="solana-wallet" />
70 + <data android:scheme="tron" />
71 + <data android:scheme="tron-wallet" />
72 + <data android:scheme="tron_wallet" />
73 </intent-filter>
74 </activity>
75 <meta-data
assets/tron_node_list.yml new
+4
@@ -0,0 +1,4 @@
1 +-
2 + uri: api.trongrid.io
3 + is_default: true
4 + useSSL: true
\ No newline at end of file
cw_core/lib/crypto_currency.dart
+2
@@ -103,6 +103,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
103 CryptoCurrency.kaspa,
104 CryptoCurrency.digibyte,
105 CryptoCurrency.usdtSol,
106 + CryptoCurrency.usdcTrc20,
107 ];
108
109 static const havenCurrencies = [
@@ -217,6 +218,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
218 static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kas', iconPath: 'assets/images/kaspa_icon.png', decimals: 8);
219 static const digibyte = CryptoCurrency(title: 'DGB', fullName: 'DigiByte', raw: 90, name: 'dgb', iconPath: 'assets/images/digibyte.png', decimals: 8);
220 static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'USDT Tether', raw: 91, name: 'usdtsol', iconPath: 'assets/images/usdt_icon.png', decimals: 6);
221 + static const usdcTrc20 = CryptoCurrency(title: 'USDC', tag: 'TRX', fullName: 'USDC Coin', raw: 92, name: 'usdctrc20', iconPath: 'assets/images/usdc_icon.png', decimals: 6);
222
223
224 static final Map<int, CryptoCurrency> _rawCurrencyMap =
cw_core/lib/currency_for_wallet_type.dart
+4 -1
@@ -23,7 +23,10 @@ CryptoCurrency currencyForWalletType(WalletType type) {
23 return CryptoCurrency.maticpoly;
24 case WalletType.solana:
25 return CryptoCurrency.sol;
26 + case WalletType.tron:
27 + return CryptoCurrency.trx;
28 default:
27 - throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType');
29 + throw Exception(
30 + 'Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType');
31 }
32 }
cw_core/lib/hive_type_ids.dart
+1
@@ -16,3 +16,4 @@ const POW_NODE_TYPE_ID = 14;
16 const DERIVATION_TYPE_TYPE_ID = 15;
17 const SPL_TOKEN_TYPE_ID = 16;
18 const DERIVATION_INFO_TYPE_ID = 17;
19 +const TRON_TOKEN_TYPE_ID = 18;
cw_core/lib/node.dart
+2
@@ -94,6 +94,7 @@ class Node extends HiveObject with Keyable {
94 case WalletType.ethereum:
95 case WalletType.polygon:
96 case WalletType.solana:
97 + case WalletType.tron:
98 return Uri.https(uriRaw, path ?? '');
99 default:
100 throw Exception('Unexpected type ${type.toString()} for Node uri');
@@ -152,6 +153,7 @@ class Node extends HiveObject with Keyable {
153 case WalletType.ethereum:
154 case WalletType.polygon:
155 case WalletType.solana:
156 + case WalletType.tron:
157 return requestElectrumServer();
158 default:
159 return false;
cw_core/lib/wallet_type.dart
+15 -1
@@ -15,6 +15,7 @@ const walletTypes = [
15 WalletType.banano,
16 WalletType.polygon,
17 WalletType.solana,
18 + WalletType.tron,
19 ];
20
21 @HiveType(typeId: WALLET_TYPE_TYPE_ID)
@@ -50,7 +51,10 @@ enum WalletType {
51 polygon,
52
53 @HiveField(10)
53 - solana
54 + solana,
55 +
56 + @HiveField(11)
57 + tron
58 }
59
60 int serializeToInt(WalletType type) {
@@ -75,6 +79,8 @@ int serializeToInt(WalletType type) {
79 return 8;
80 case WalletType.solana:
81 return 9;
82 + case WalletType.tron:
83 + return 10;
84 default:
85 return -1;
86 }
@@ -102,6 +108,8 @@ WalletType deserializeFromInt(int raw) {
108 return WalletType.polygon;
109 case 9:
110 return WalletType.solana;
111 + case 10:
112 + return WalletType.tron;
113 default:
114 throw Exception('Unexpected token: $raw for WalletType deserializeFromInt');
115 }
@@ -129,6 +137,8 @@ String walletTypeToString(WalletType type) {
137 return 'Polygon';
138 case WalletType.solana:
139 return 'Solana';
140 + case WalletType.tron:
141 + return 'Tron';
142 default:
143 return '';
144 }
@@ -156,6 +166,8 @@ String walletTypeToDisplayName(WalletType type) {
166 return 'Polygon (MATIC)';
167 case WalletType.solana:
168 return 'Solana (SOL)';
169 + case WalletType.tron:
170 + return 'Tron (TRX)';
171 default:
172 return '';
173 }
@@ -183,6 +195,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) {
195 return CryptoCurrency.maticpoly;
196 case WalletType.solana:
197 return CryptoCurrency.sol;
198 + case WalletType.tron:
199 + return CryptoCurrency.trx;
200 default:
201 throw Exception(
202 'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency');
cw_tron/.gitignore new
+30
@@ -0,0 +1,30 @@
1 +# Miscellaneous
2 +*.class
3 +*.log
4 +*.pyc
5 +*.swp
6 +.DS_Store
7 +.atom/
8 +.buildlog/
9 +.history
10 +.svn/
11 +migrate_working_dir/
12 +
13 +# IntelliJ related
14 +*.iml
15 +*.ipr
16 +*.iws
17 +.idea/
18 +
19 +# The .vscode folder contains launch configuration and tasks you configure in
20 +# VS Code which you may wish to be included in version control, so this line
21 +# is commented out by default.
22 +#.vscode/
23 +
24 +# Flutter/Dart/Pub related
25 +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
26 +/pubspec.lock
27 +**/doc/api/
28 +.dart_tool/
29 +.packages
30 +build/
cw_tron/.metadata new
+10
@@ -0,0 +1,10 @@
1 +# This file tracks properties of this Flutter project.
2 +# Used by Flutter tool to assess capabilities and perform upgrades etc.
3 +#
4 +# This file should be version controlled and should not be manually edited.
5 +
6 +version:
7 + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
8 + channel: stable
9 +
10 +project_type: package
cw_tron/CHANGELOG.md new
+3
@@ -0,0 +1,3 @@
1 +## 0.0.1
2 +
3 +* TODO: Describe initial release.
cw_tron/LICENSE new
+1
@@ -0,0 +1 @@
1 +TODO: Add your license here.
cw_tron/README.md new
+39
@@ -0,0 +1,39 @@
1 +<!--
2 +This README describes the package. If you publish this package to pub.dev,
3 +this README's contents appear on the landing page for your package.
4 +
5 +For information about how to write a good package README, see the guide for
6 +[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
7 +
8 +For general information about developing packages, see the Dart guide for
9 +[creating packages](https://dart.dev/guides/libraries/create-library-packages)
10 +and the Flutter guide for
11 +[developing packages and plugins](https://flutter.dev/developing-packages).
12 +-->
13 +
14 +TODO: Put a short description of the package here that helps potential users
15 +know whether this package might be useful for them.
16 +
17 +## Features
18 +
19 +TODO: List what your package can do. Maybe include images, gifs, or videos.
20 +
21 +## Getting started
22 +
23 +TODO: List prerequisites and provide or point to information on how to
24 +start using the package.
25 +
26 +## Usage
27 +
28 +TODO: Include short and useful examples for package users. Add longer examples
29 +to `/example` folder.
30 +
31 +```dart
32 +const like = 'sample';
33 +```
34 +
35 +## Additional information
36 +
37 +TODO: Tell users more about the package: where to find more information, how to
38 +contribute to the package, how to file issues, what response they can expect
39 +from the package authors, and more.
cw_tron/analysis_options.yaml new
+4
@@ -0,0 +1,4 @@
1 +include: package:flutter_lints/flutter.yaml
2 +
3 +# Additional information about this file can be found at
4 +# https://dart.dev/guides/language/analysis-options
cw_tron/lib/cw_tron.dart new
+7
@@ -0,0 +1,7 @@
1 +library cw_tron;
2 +
3 +/// A Calculator.
4 +class Calculator {
5 + /// Returns [value] plus 1.
6 + int addOne(int value) => value + 1;
7 +}
cw_tron/lib/default_tron_tokens.dart new
+103
@@ -0,0 +1,103 @@
1 +import 'package:cw_core/crypto_currency.dart';
2 +import 'package:cw_tron/tron_token.dart';
3 +
4 +class DefaultTronTokens {
5 + final List<TronToken> _defaultTokens = [
6 + TronToken(
7 + name: "Tether USD",
8 + symbol: "USDT",
9 + contractAddress: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
10 + decimal: 6,
11 + enabled: true,
12 + ),
13 + TronToken(
14 + name: "USD Coin",
15 + symbol: "USDC",
16 + contractAddress: "TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8",
17 + decimal: 6,
18 + enabled: true,
19 + ),
20 + TronToken(
21 + name: "Bitcoin",
22 + symbol: "BTC",
23 + contractAddress: "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9",
24 + decimal: 8,
25 + enabled: true,
26 + ),
27 + TronToken(
28 + name: "Ethereum",
29 + symbol: "ETH",
30 + contractAddress: "TRFe3hT5oYhjSZ6f3ji5FJ7YCfrkWnHRvh",
31 + decimal: 18,
32 + enabled: true,
33 + ),
34 + TronToken(
35 + name: "Wrapped BTC",
36 + symbol: "WBTC",
37 + contractAddress: "TXpw8XeWYeTUd4quDskoUqeQPowRh4jY65",
38 + decimal: 8,
39 + enabled: true,
40 + ),
41 + TronToken(
42 + name: "Dogecoin",
43 + symbol: "DOGE",
44 + contractAddress: "THbVQp8kMjStKNnf2iCY6NEzThKMK5aBHg",
45 + decimal: 8,
46 + enabled: true,
47 + ),
48 + TronToken(
49 + name: "JUST Stablecoin",
50 + symbol: "USDJ",
51 + contractAddress: "TMwFHYXLJaRUPeW6421aqXL4ZEzPRFGkGT",
52 + decimal: 18,
53 + enabled: false,
54 + ),
55 + TronToken(
56 + name: "SUN",
57 + symbol: "SUN",
58 + contractAddress: "TSSMHYeV2uE9qYH95DqyoCuNCzEL1NvU3S",
59 + decimal: 18,
60 + enabled: false,
61 + ),
62 + TronToken(
63 + name: "Wrapped TRX",
64 + symbol: "WTRX",
65 + contractAddress: "TNUC9Qb1rRpS5CbWLmNMxXBjyFoydXjWFR",
66 + decimal: 6,
67 + enabled: false,
68 + ),
69 + TronToken(
70 + name: "BitTorent",
71 + symbol: "BTT",
72 + contractAddress: "TAFjULxiVgT4qWk6UZwjqwZXTSaGaqnVp4",
73 + decimal: 18,
74 + enabled: false,
75 + ),
76 + TronToken(
77 + name: "BUSD Token",
78 + symbol: "BUSD",
79 + contractAddress: "TMz2SWatiAtZVVcH2ebpsbVtYwUPT9EdjH",
80 + decimal: 18,
81 + enabled: false,
82 + ),
83 + TronToken(
84 + name: "HTX",
85 + symbol: "HTX",
86 + contractAddress: "TUPM7K8REVzD2UdV4R5fe5M8XbnR2DdoJ6",
87 + decimal: 18,
88 + enabled: false,
89 + ),
90 + ];
91 +
92 + List<TronToken> get initialTronTokens => _defaultTokens.map((token) {
93 + String? iconPath;
94 + try {
95 + iconPath = CryptoCurrency.all
96 + .firstWhere((element) =>
97 + element.title.toUpperCase() == token.symbol.split(".").first.toUpperCase())
98 + .iconPath;
99 + } catch (_) {}
100 +
101 + return TronToken.copyWith(token, iconPath, 'TRX');
102 + }).toList();
103 +}
cw_tron/lib/file.dart new
+39
@@ -0,0 +1,39 @@
1 +import 'dart:io';
2 +import 'package:cw_core/key.dart';
3 +import 'package:encrypt/encrypt.dart' as encrypt;
4 +
5 +Future<void> write(
6 + {required String path,
7 + required String password,
8 + required String data}) async {
9 + final keys = extractKeys(password);
10 + final key = encrypt.Key.fromBase64(keys.first);
11 + final iv = encrypt.IV.fromBase64(keys.last);
12 + final encrypted = await encode(key: key, iv: iv, data: data);
13 + final f = File(path);
14 + f.writeAsStringSync(encrypted);
15 +}
16 +
17 +Future<void> writeData(
18 + {required String path,
19 + required String password,
20 + required String data}) async {
21 + final keys = extractKeys(password);
22 + final key = encrypt.Key.fromBase64(keys.first);
23 + final iv = encrypt.IV.fromBase64(keys.last);
24 + final encrypted = await encode(key: key, iv: iv, data: data);
25 + final f = File(path);
26 + f.writeAsStringSync(encrypted);
27 +}
28 +
29 +Future<String> read({required String path, required String password}) async {
30 + final file = File(path);
31 +
32 + if (!file.existsSync()) {
33 + file.createSync();
34 + }
35 +
36 + final encrypted = file.readAsStringSync();
37 +
38 + return decode(password: password, data: encrypted);
39 +}
cw_tron/lib/pending_tron_transaction.dart new
+33
@@ -0,0 +1,33 @@
1 +
2 +
3 +import 'package:cw_core/pending_transaction.dart';
4 +import 'package:web3dart/crypto.dart';
5 +
6 +class PendingTronTransaction with PendingTransaction {
7 + final Function sendTransaction;
8 + final List<int> signedTransaction;
9 + final String fee;
10 + final String amount;
11 +
12 + PendingTronTransaction({
13 + required this.sendTransaction,
14 + required this.signedTransaction,
15 + required this.fee,
16 + required this.amount,
17 + });
18 +
19 + @override
20 + String get amountFormatted => amount;
21 +
22 + @override
23 + Future<void> commit() async => await sendTransaction();
24 +
25 + @override
26 + String get feeFormatted => fee;
27 +
28 + @override
29 + String get hex => bytesToHex(signedTransaction);
30 +
31 + @override
32 + String get id => '';
33 +}
cw_tron/lib/tron_abi.dart new
+436
@@ -0,0 +1,436 @@
1 +final trc20Abi = [
2 + {"inputs": [], "stateMutability": "nonpayable", "type": "constructor"},
3 + {
4 + "anonymous": false,
5 + "inputs": [
6 + {"indexed": true, "internalType": "address", "name": "owner", "type": "address"},
7 + {"indexed": true, "internalType": "address", "name": "spender", "type": "address"},
8 + {"indexed": false, "internalType": "uint256", "name": "value", "type": "uint256"}
9 + ],
10 + "name": "Approval",
11 + "type": "event"
12 + },
13 + {
14 + "anonymous": false,
15 + "inputs": [
16 + {"indexed": false, "internalType": "uint256", "name": "total", "type": "uint256"},
17 + {"indexed": true, "internalType": "uint16", "name": "order_id", "type": "uint16"},
18 + {"indexed": true, "internalType": "address", "name": "buyer", "type": "address"},
19 + {"indexed": true, "internalType": "address", "name": "seller", "type": "address"},
20 + {"indexed": false, "internalType": "address", "name": "contract_address", "type": "address"}
21 + ],
22 + "name": "OrderPaid",
23 + "type": "event"
24 + },
25 + {
26 + "anonymous": false,
27 + "inputs": [
28 + {"indexed": true, "internalType": "address", "name": "previousOwner", "type": "address"},
29 + {"indexed": true, "internalType": "address", "name": "newOwner", "type": "address"}
30 + ],
31 + "name": "OwnershipTransferred",
32 + "type": "event"
33 + },
34 + {
35 + "anonymous": false,
36 + "inputs": [
37 + {"indexed": false, "internalType": "address", "name": "token", "type": "address"},
38 + {"indexed": false, "internalType": "bool", "name": "active", "type": "bool"}
39 + ],
40 + "name": "TokenUpdate",
41 + "type": "event"
42 + },
43 + {
44 + "anonymous": false,
45 + "inputs": [
46 + {"indexed": true, "internalType": "address", "name": "from", "type": "address"},
47 + {"indexed": true, "internalType": "address", "name": "to", "type": "address"},
48 + {"indexed": false, "internalType": "uint256", "name": "value", "type": "uint256"}
49 + ],
50 + "name": "Transfer",
51 + "type": "event"
52 + },
53 + {
54 + "anonymous": false,
55 + "inputs": [
56 + {"indexed": false, "internalType": "string", "name": "username", "type": "string"},
57 + {"indexed": true, "internalType": "address", "name": "seller", "type": "address"}
58 + ],
59 + "name": "UserRegistred",
60 + "type": "event"
61 + },
62 + {
63 + "anonymous": false,
64 + "inputs": [
65 + {"indexed": true, "internalType": "uint16", "name": "order_id", "type": "uint16"},
66 + {"indexed": true, "internalType": "address", "name": "buyer", "type": "address"},
67 + {"indexed": false, "internalType": "address", "name": "seller", "type": "address"}
68 + ],
69 + "name": "WBuyer",
70 + "type": "event"
71 + },
72 + {
73 + "anonymous": false,
74 + "inputs": [
75 + {"indexed": true, "internalType": "uint16", "name": "order_id", "type": "uint16"},
76 + {"indexed": true, "internalType": "address", "name": "seller", "type": "address"},
77 + {"indexed": false, "internalType": "address", "name": "buyer", "type": "address"}
78 + ],
79 + "name": "WSeller",
80 + "type": "event"
81 + },
82 + {
83 + "inputs": [],
84 + "name": "CONTRACTPERCENTAGE",
85 + "outputs": [
86 + {"internalType": "uint8", "name": "", "type": "uint8"}
87 + ],
88 + "stateMutability": "view",
89 + "type": "function"
90 + },
91 + {
92 + "inputs": [
93 + {"internalType": "uint16", "name": "order_id", "type": "uint16"},
94 + {"internalType": "uint256", "name": "order_total", "type": "uint256"},
95 + {"internalType": "address", "name": "contractAddress", "type": "address"},
96 + {"internalType": "address", "name": "seller", "type": "address"}
97 + ],
98 + "name": "PayWithTokens",
99 + "outputs": [],
100 + "stateMutability": "nonpayable",
101 + "type": "function"
102 + },
103 + {
104 + "inputs": [],
105 + "name": "TOKENINCREAMENT",
106 + "outputs": [
107 + {"internalType": "uint16", "name": "", "type": "uint16"}
108 + ],
109 + "stateMutability": "view",
110 + "type": "function"
111 + },
112 + {
113 + "inputs": [
114 + {"internalType": "address", "name": "", "type": "address"}
115 + ],
116 + "name": "_signer",
117 + "outputs": [
118 + {"internalType": "bool", "name": "", "type": "bool"}
119 + ],
120 + "stateMutability": "view",
121 + "type": "function"
122 + },
123 + {
124 + "inputs": [
125 + {"internalType": "address", "name": "", "type": "address"}
126 + ],
127 + "name": "_tokens",
128 + "outputs": [
129 + {"internalType": "bool", "name": "active", "type": "bool"},
130 + {"internalType": "uint16", "name": "token", "type": "uint16"}
131 + ],
132 + "stateMutability": "view",
133 + "type": "function"
134 + },
135 + {
136 + "inputs": [
137 + {"internalType": "address", "name": "", "type": "address"}
138 + ],
139 + "name": "_users",
140 + "outputs": [
141 + {"internalType": "bool", "name": "active", "type": "bool"}
142 + ],
143 + "stateMutability": "view",
144 + "type": "function"
145 + },
146 + {
147 + "inputs": [
148 + {"internalType": "address", "name": "owner", "type": "address"},
149 + {"internalType": "address", "name": "spender", "type": "address"}
150 + ],
151 + "name": "allowance",
152 + "outputs": [
153 + {"internalType": "uint256", "name": "", "type": "uint256"}
154 + ],
155 + "stateMutability": "view",
156 + "type": "function"
157 + },
158 + {
159 + "inputs": [
160 + {"internalType": "address", "name": "spender", "type": "address"},
161 + {"internalType": "uint256", "name": "amount", "type": "uint256"}
162 + ],
163 + "name": "approve",
164 + "outputs": [
165 + {"internalType": "bool", "name": "", "type": "bool"}
166 + ],
167 + "stateMutability": "nonpayable",
168 + "type": "function"
169 + },
170 + {
171 + "inputs": [
172 + {"internalType": "address", "name": "account", "type": "address"}
173 + ],
174 + "name": "balanceOf",
175 + "outputs": [
176 + {"internalType": "uint256", "name": "", "type": "uint256"}
177 + ],
178 + "stateMutability": "view",
179 + "type": "function"
180 + },
181 + {
182 + "inputs": [
183 + {"internalType": "address", "name": "token", "type": "address"}
184 + ],
185 + "name": "balanceOfContract",
186 + "outputs": [
187 + {"internalType": "uint256", "name": "", "type": "uint256"}
188 + ],
189 + "stateMutability": "view",
190 + "type": "function"
191 + },
192 + {
193 + "inputs": [
194 + {"internalType": "uint256", "name": "amount", "type": "uint256"}
195 + ],
196 + "name": "burn",
197 + "outputs": [],
198 + "stateMutability": "nonpayable",
199 + "type": "function"
200 + },
201 + {
202 + "inputs": [
203 + {"internalType": "address", "name": "account", "type": "address"},
204 + {"internalType": "uint256", "name": "amount", "type": "uint256"}
205 + ],
206 + "name": "burnFrom",
207 + "outputs": [],
208 + "stateMutability": "nonpayable",
209 + "type": "function"
210 + },
211 + {
212 + "inputs": [
213 + {"internalType": "uint256", "name": "value", "type": "uint256"},
214 + {"internalType": "address", "name": "_contractAddress", "type": "address"}
215 + ],
216 + "name": "contractWithdraw",
217 + "outputs": [],
218 + "stateMutability": "nonpayable",
219 + "type": "function"
220 + },
221 + {
222 + "inputs": [],
223 + "name": "decimals",
224 + "outputs": [
225 + {"internalType": "uint8", "name": "", "type": "uint8"}
226 + ],
227 + "stateMutability": "view",
228 + "type": "function"
229 + },
230 + {
231 + "inputs": [
232 + {"internalType": "address", "name": "spender", "type": "address"},
233 + {"internalType": "uint256", "name": "subtractedValue", "type": "uint256"}
234 + ],
235 + "name": "decreaseAllowance",
236 + "outputs": [
237 + {"internalType": "bool", "name": "", "type": "bool"}
238 + ],
239 + "stateMutability": "nonpayable",
240 + "type": "function"
241 + },
242 + {
243 + "inputs": [
244 + {"internalType": "address", "name": "spender", "type": "address"},
245 + {"internalType": "uint256", "name": "addedValue", "type": "uint256"}
246 + ],
247 + "name": "increaseAllowance",
248 + "outputs": [
249 + {"internalType": "bool", "name": "", "type": "bool"}
250 + ],
251 + "stateMutability": "nonpayable",
252 + "type": "function"
253 + },
254 + {
255 + "inputs": [
256 + {"internalType": "address", "name": "to", "type": "address"},
257 + {"internalType": "uint256", "name": "amount", "type": "uint256"}
258 + ],
259 + "name": "mint",
260 + "outputs": [],
261 + "stateMutability": "nonpayable",
262 + "type": "function"
263 + },
264 + {
265 + "inputs": [],
266 + "name": "name",
267 + "outputs": [
268 + {"internalType": "string", "name": "", "type": "string"}
269 + ],
270 + "stateMutability": "view",
271 + "type": "function"
272 + },
273 + {
274 + "inputs": [],
275 + "name": "owner",
276 + "outputs": [
277 + {"internalType": "address", "name": "", "type": "address"}
278 + ],
279 + "stateMutability": "view",
280 + "type": "function"
281 + },
282 + {
283 + "inputs": [
284 + {"internalType": "address", "name": "token", "type": "address"},
285 + {"internalType": "uint256", "name": "value", "type": "uint256"}
286 + ],
287 + "name": "payToContract",
288 + "outputs": [],
289 + "stateMutability": "payable",
290 + "type": "function"
291 + },
292 + {
293 + "inputs": [
294 + {"internalType": "uint16", "name": "order_id", "type": "uint16"},
295 + {"internalType": "address", "name": "seller", "type": "address"}
296 + ],
297 + "name": "payWithNativeToken",
298 + "outputs": [],
299 + "stateMutability": "payable",
300 + "type": "function"
301 + },
302 + {
303 + "inputs": [
304 + {"internalType": "string", "name": "username", "type": "string"}
305 + ],
306 + "name": "regiserUser",
307 + "outputs": [],
308 + "stateMutability": "nonpayable",
309 + "type": "function"
310 + },
311 + {
312 + "inputs": [],
313 + "name": "renounceOwnership",
314 + "outputs": [],
315 + "stateMutability": "nonpayable",
316 + "type": "function"
317 + },
318 + {
319 + "inputs": [
320 + {"internalType": "uint16", "name": "id", "type": "uint16"},
321 + {"internalType": "address", "name": "buyer", "type": "address"},
322 + {"internalType": "address", "name": "seller", "type": "address"}
323 + ],
324 + "name": "selectOrder",
325 + "outputs": [
326 + {"internalType": "uint232", "name": "", "type": "uint232"},
327 + {"internalType": "uint16", "name": "", "type": "uint16"},
328 + {"internalType": "uint8", "name": "", "type": "uint8"}
329 + ],
330 + "stateMutability": "view",
331 + "type": "function"
332 + },
333 + {
334 + "inputs": [],
335 + "name": "symbol",
336 + "outputs": [
337 + {"internalType": "string", "name": "", "type": "string"}
338 + ],
339 + "stateMutability": "view",
340 + "type": "function"
341 + },
342 + {
343 + "inputs": [
344 + {"internalType": "address", "name": "signer", "type": "address"}
345 + ],
346 + "name": "toggleSigner",
347 + "outputs": [],
348 + "stateMutability": "nonpayable",
349 + "type": "function"
350 + },
351 + {
352 + "inputs": [
353 + {"internalType": "address", "name": "tokenAddress", "type": "address"}
354 + ],
355 + "name": "toggleToken",
356 + "outputs": [],
357 + "stateMutability": "nonpayable",
358 + "type": "function"
359 + },
360 + {
361 + "inputs": [],
362 + "name": "totalSupply",
363 + "outputs": [
364 + {"internalType": "uint256", "name": "", "type": "uint256"}
365 + ],
366 + "stateMutability": "view",
367 + "type": "function"
368 + },
369 + {
370 + "inputs": [
371 + {"internalType": "address", "name": "to", "type": "address"},
372 + {"internalType": "uint256", "name": "amount", "type": "uint256"}
373 + ],
374 + "name": "transfer",
375 + "outputs": [
376 + {"internalType": "bool", "name": "", "type": "bool"}
377 + ],
378 + "stateMutability": "nonpayable",
379 + "type": "function"
380 + },
381 + {
382 + "inputs": [
383 + {"internalType": "address", "name": "from", "type": "address"},
384 + {"internalType": "address", "name": "to", "type": "address"},
385 + {"internalType": "uint256", "name": "amount", "type": "uint256"}
386 + ],
387 + "name": "transferFrom",
388 + "outputs": [
389 + {"internalType": "bool", "name": "", "type": "bool"}
390 + ],
391 + "stateMutability": "nonpayable",
392 + "type": "function"
393 + },
394 + {
395 + "inputs": [
396 + {"internalType": "address", "name": "newOwner", "type": "address"}
397 + ],
398 + "name": "transferOwnership",
399 + "outputs": [],
400 + "stateMutability": "nonpayable",
401 + "type": "function"
402 + },
403 + {
404 + "inputs": [
405 + {"internalType": "uint8", "name": "newPercentage", "type": "uint8"}
406 + ],
407 + "name": "updateContractPercentage",
408 + "outputs": [],
409 + "stateMutability": "nonpayable",
410 + "type": "function"
411 + },
412 + {
413 + "inputs": [
414 + {"internalType": "address[]", "name": "buyer", "type": "address[]"},
415 + {"internalType": "bytes[]", "name": "signature", "type": "bytes[]"},
416 + {"internalType": "uint16[]", "name": "order_id", "type": "uint16[]"},
417 + {"internalType": "address", "name": "contractAddress", "type": "address"}
418 + ],
419 + "name": "widthrawForSellers",
420 + "outputs": [],
421 + "stateMutability": "nonpayable",
422 + "type": "function"
423 + },
424 + {
425 + "inputs": [
426 + {"internalType": "address", "name": "seller", "type": "address"},
427 + {"internalType": "bytes", "name": "signature", "type": "bytes"},
428 + {"internalType": "uint16", "name": "order_id", "type": "uint16"},
429 + {"internalType": "address", "name": "contractAddress", "type": "address"}
430 + ],
431 + "name": "widthrowForBuyers",
432 + "outputs": [],
433 + "stateMutability": "nonpayable",
434 + "type": "function"
435 + }
436 +];
cw_tron/lib/tron_balance.dart new
+34
@@ -0,0 +1,34 @@
1 +import 'dart:convert';
2 +
3 +import 'package:cw_core/balance.dart';
4 +import 'package:on_chain/on_chain.dart';
5 +
6 +class TronBalance extends Balance {
7 + TronBalance(this.balance) : super(balance.toInt(), balance.toInt());
8 +
9 + final BigInt balance;
10 +
11 + @override
12 + String get formattedAdditionalBalance => TronHelper.fromSun(balance);
13 +
14 + @override
15 + String get formattedAvailableBalance => TronHelper.fromSun(balance);
16 +
17 + String toJSON() => json.encode({
18 + 'balance': balance.toString(),
19 + });
20 +
21 + static TronBalance? fromJSON(String? jsonSource) {
22 + if (jsonSource == null) {
23 + return null;
24 + }
25 +
26 + final decoded = json.decode(jsonSource) as Map;
27 +
28 + try {
29 + return TronBalance(BigInt.parse(decoded['balance']));
30 + } catch (e) {
31 + return TronBalance(BigInt.zero);
32 + }
33 + }
34 +}
cw_tron/lib/tron_client.dart new
+574
@@ -0,0 +1,574 @@
1 +import 'dart:async';
2 +import 'dart:convert';
3 +import 'dart:developer';
4 +
5 +import 'package:blockchain_utils/blockchain_utils.dart';
6 +import 'package:cw_core/crypto_currency.dart';
7 +import 'package:cw_core/node.dart';
8 +import 'package:cw_tron/pending_tron_transaction.dart';
9 +import 'package:cw_tron/tron_abi.dart';
10 +import 'package:cw_tron/tron_balance.dart';
11 +import 'package:cw_tron/tron_http_provider.dart';
12 +import 'package:cw_tron/tron_token.dart';
13 +import 'package:cw_tron/tron_transaction_model.dart';
14 +import 'package:flutter/foundation.dart';
15 +import 'package:flutter/services.dart';
16 +import 'package:http/http.dart';
17 +import '.secrets.g.dart' as secrets;
18 +import 'package:on_chain/on_chain.dart';
19 +
20 +class TronClient {
21 + final httpClient = Client();
22 + TronProvider? _provider;
23 + // This is an internal tracker, so we don't have to "refetch".
24 + int _nativeTxEstimatedFee = 0;
25 +
26 + int get chainId => 1000;
27 +
28 + Future<List<TronTransactionModel>> fetchTransactions(String address,
29 + {String? contractAddress}) async {
30 + try {
31 + final response = await httpClient.get(
32 + Uri.https(
33 + "api.trongrid.io",
34 + "/v1/accounts/$address/transactions",
35 + {
36 + "only_confirmed": "true",
37 + "limit": "200",
38 + },
39 + ),
40 + headers: {
41 + 'Content-Type': 'application/json',
42 + 'TRON-PRO-API-KEY': secrets.tronGridApiKey,
43 + },
44 + );
45 + final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
46 +
47 + if (response.statusCode >= 200 &&
48 + response.statusCode < 300 &&
49 + jsonResponse['status'] != false) {
50 + return (jsonResponse['data'] as List).map((e) {
51 + return TronTransactionModel.fromJson(e as Map<String, dynamic>);
52 + }).toList();
53 + }
54 +
55 + return [];
56 + } catch (e, s) {
57 + log('Error getting tx: ${e.toString()}\n ${s.toString()}');
58 + return [];
59 + }
60 + }
61 +
62 + Future<List<TronTRC20TransactionModel>> fetchTrc20ExcludedTransactions(String address) async {
63 + try {
64 + final response = await httpClient.get(
65 + Uri.https(
66 + "api.trongrid.io",
67 + "/v1/accounts/$address/transactions/trc20",
68 + {
69 + "only_confirmed": "true",
70 + "limit": "200",
71 + },
72 + ),
73 + headers: {
74 + 'Content-Type': 'application/json',
75 + 'TRON-PRO-API-KEY': secrets.tronGridApiKey,
76 + },
77 + );
78 + final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
79 +
80 + if (response.statusCode >= 200 &&
81 + response.statusCode < 300 &&
82 + jsonResponse['status'] != false) {
83 + return (jsonResponse['data'] as List).map((e) {
84 + return TronTRC20TransactionModel.fromJson(e as Map<String, dynamic>);
85 + }).toList();
86 + }
87 +
88 + return [];
89 + } catch (e, s) {
90 + log('Error getting trc20 tx: ${e.toString()}\n ${s.toString()}');
91 + return [];
92 + }
93 + }
94 +
95 + bool connect(Node node) {
96 + try {
97 + final formattedUrl = '${node.isSSL ? 'https' : 'http'}://${node.uriRaw}';
98 + _provider = TronProvider(TronHTTPProvider(url: formattedUrl));
99 +
100 + return true;
101 + } catch (e) {
102 + return false;
103 + }
104 + }
105 +
106 + Future<BigInt> getBalance(TronAddress address) async {
107 + try {
108 + final accountDetails = await _provider!.request(TronRequestGetAccount(address: address));
109 +
110 + return accountDetails?.balance ?? BigInt.zero;
111 + } catch (_) {
112 + return BigInt.zero;
113 + }
114 + }
115 +
116 + Future<int> getFeeLimit(
117 + TransactionRaw rawTransaction,
118 + TronAddress address,
119 + TronAddress receiverAddress, {
120 + int energyUsed = 0,
121 + bool isEstimatedFeeFlow = false,
122 + }) async {
123 + try {
124 + // Get the tron chain parameters.
125 + final chainParams = await _provider!.request(TronRequestGetChainParameters());
126 +
127 + final bandWidthInSun = chainParams.getTransactionFee!;
128 + log('BandWidth In Sun: $bandWidthInSun');
129 +
130 + final energyInSun = chainParams.getEnergyFee!;
131 + log('Energy In Sun: $energyInSun');
132 +
133 + log(
134 + 'Create Account Fee In System Contract for Chain: ${chainParams.getCreateNewAccountFeeInSystemContract!}',
135 + );
136 + log('Create Account Fee for Chain: ${chainParams.getCreateAccountFee}');
137 +
138 + final fakeTransaction = Transaction(
139 + rawData: rawTransaction,
140 + signature: [Uint8List(65)],
141 + );
142 +
143 + // Calculate the total size of the fake transaction, considering the required network overhead.
144 + final transactionSize = fakeTransaction.length + 64;
145 +
146 + // Assign the calculated size to the variable representing the required bandwidth.
147 + int neededBandWidth = transactionSize;
148 + log('Initial Needed Bandwidth: $neededBandWidth');
149 +
150 + int neededEnergy = energyUsed;
151 + log('Initial Needed Energy: $neededEnergy');
152 +
153 + // Fetch account resources to assess the available bandwidth and energy
154 + final accountResource =
155 + await _provider!.request(TronRequestGetAccountResource(address: address));
156 +
157 + neededEnergy -= accountResource.howManyEnergy.toInt();
158 + log('Account resource energy: ${accountResource.howManyEnergy.toInt()}');
159 + log('Needed Energy after deducting from account resource energy: $neededEnergy');
160 +
161 + // Deduct the bandwidth from the account's available bandwidth.
162 + final BigInt accountBandWidth = accountResource.howManyBandwIth;
163 + log('Account resource bandwidth: ${accountResource.howManyBandwIth.toInt()}');
164 +
165 + if (accountBandWidth >= BigInt.from(neededBandWidth) && !isEstimatedFeeFlow) {
166 + log('Account has more bandwidth than required');
167 + neededBandWidth = 0;
168 + }
169 +
170 + if (neededEnergy < 0) {
171 + neededEnergy = 0;
172 + }
173 +
174 + final energyBurn = neededEnergy * energyInSun.toInt();
175 + log('Energy Burn: $energyBurn');
176 +
177 + final bandWidthBurn = neededBandWidth * bandWidthInSun;
178 + log('Bandwidth Burn: $bandWidthBurn');
179 +
180 + int totalBurn = energyBurn + bandWidthBurn;
181 + log('Total Burn: $totalBurn');
182 +
183 + /// If there is a note (memo), calculate the memo fee.
184 + if (rawTransaction.data != null) {
185 + totalBurn += chainParams.getMemoFee!;
186 + }
187 +
188 + // Check if receiver's account is active
189 + final receiverAccountInfo =
190 + await _provider!.request(TronRequestGetAccount(address: receiverAddress));
191 +
192 + /// Calculate the resources required to create a new account.
193 + if (receiverAccountInfo == null) {
194 + totalBurn += chainParams.getCreateNewAccountFeeInSystemContract!;
195 +
196 + totalBurn += (chainParams.getCreateAccountFee! * bandWidthInSun);
197 + }
198 +
199 + log('Final total burn: $totalBurn');
200 +
201 + return totalBurn;
202 + } catch (_) {
203 + return 0;
204 + }
205 + }
206 +
207 + Future<int> getEstimatedFee(TronAddress ownerAddress) async {
208 + const constantAmount = '1000';
209 + // Fetch the latest Tron block
210 + final block = await _provider!.request(TronRequestGetNowBlock());
211 +
212 + // Create the transfer contract
213 + final contract = TransferContract(
214 + amount: TronHelper.toSun(constantAmount),
215 + ownerAddress: ownerAddress,
216 + toAddress: ownerAddress,
217 + );
218 +
219 + // Prepare the contract parameter for the transaction.
220 + final parameter = Any(typeUrl: contract.typeURL, value: contract);
221 +
222 + // Create a TransactionContract object with the contract type and parameter.
223 + final transactionContract =
224 + TransactionContract(type: contract.contractType, parameter: parameter);
225 +
226 + // Set the transaction expiration time (maximum 24 hours)
227 + final expireTime = DateTime.now().toUtc().add(const Duration(hours: 24));
228 +
229 + // Create a raw transaction
230 + TransactionRaw rawTransaction = TransactionRaw(
231 + refBlockBytes: block.blockHeader.rawData.refBlockBytes,
232 + refBlockHash: block.blockHeader.rawData.refBlockHash,
233 + expiration: BigInt.from(expireTime.millisecondsSinceEpoch),
234 + contract: [transactionContract],
235 + timestamp: block.blockHeader.rawData.timestamp,
236 + );
237 +
238 + final estimatedFee = await getFeeLimit(
239 + rawTransaction,
240 + ownerAddress,
241 + ownerAddress,
242 + isEstimatedFeeFlow: true,
243 + );
244 +
245 + _nativeTxEstimatedFee = estimatedFee;
246 +
247 + return estimatedFee;
248 + }
249 +
250 + Future<int> getTRCEstimatedFee(TronAddress ownerAddress) async {
251 + String contractAddress = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t';
252 + String constantAmount =
253 + '0'; // We're using 0 as the base amount here as we get an error when balance is zero i.e for new wallets.
254 + final contract = ContractABI.fromJson(trc20Abi, isTron: true);
255 +
256 + final function = contract.functionFromName("transfer");
257 +
258 + /// address /// amount
259 + final transferparams = [
260 + ownerAddress,
261 + TronHelper.toSun(constantAmount),
262 + ];
263 +
264 + final contractAddr = TronAddress(contractAddress);
265 +
266 + final request = await _provider!.request(
267 + TronRequestTriggerConstantContract(
268 + ownerAddress: ownerAddress,
269 + contractAddress: contractAddr,
270 + data: function.encodeHex(transferparams),
271 + ),
272 + );
273 +
274 + if (!request.isSuccess) {
275 + log("Tron TRC20 error: ${request.error} \n ${request.respose}");
276 + }
277 +
278 + final feeLimit = await getFeeLimit(
279 + request.transactionRaw!,
280 + ownerAddress,
281 + ownerAddress,
282 + energyUsed: request.energyUsed ?? 0,
283 + isEstimatedFeeFlow: true,
284 + );
285 + return feeLimit;
286 + }
287 +
288 + Future<PendingTronTransaction> signTransaction({
289 + required TronPrivateKey ownerPrivKey,
290 + required String toAddress,
291 + required String amount,
292 + required CryptoCurrency currency,
293 + required BigInt tronBalance,
294 + required bool sendAll,
295 + }) async {
296 + // Get the owner tron address from the key
297 + final ownerAddress = ownerPrivKey.publicKey().toAddress();
298 +
299 + // Define the receiving Tron address for the transaction.
300 + final receiverAddress = TronAddress(toAddress);
301 +
302 + bool isNativeTransaction = currency == CryptoCurrency.trx;
303 +
304 + String totalAmount;
305 + TransactionRaw rawTransaction;
306 + if (isNativeTransaction) {
307 + if (sendAll) {
308 + final accountResource =
309 + await _provider!.request(TronRequestGetAccountResource(address: ownerAddress));
310 +
311 + final availableBandWidth = accountResource.howManyBandwIth.toInt();
312 +
313 + // 269 is the current middle ground for bandwidth per transaction
314 + if (availableBandWidth >= 269) {
315 + totalAmount = amount;
316 + } else {
317 + final amountInSun = TronHelper.toSun(amount).toInt();
318 +
319 + // 5000 added here is a buffer since we're working with "estimated" value of the fee.
320 + final result = amountInSun - (_nativeTxEstimatedFee + 5000);
321 +
322 + totalAmount = TronHelper.fromSun(BigInt.from(result));
323 + }
324 + } else {
325 + totalAmount = amount;
326 + }
327 + rawTransaction = await _signNativeTransaction(
328 + ownerAddress,
329 + receiverAddress,
330 + totalAmount,
331 + tronBalance,
332 + sendAll,
333 + );
334 + } else {
335 + final tokenAddress = (currency as TronToken).contractAddress;
336 + totalAmount = amount;
337 + rawTransaction = await _signTrcTokenTransaction(
338 + ownerAddress,
339 + receiverAddress,
340 + totalAmount,
341 + tokenAddress,
342 + tronBalance,
343 + );
344 + }
345 +
346 + final signature = ownerPrivKey.sign(rawTransaction.toBuffer());
347 +
348 + sendTx() async => await sendTransaction(
349 + rawTransaction: rawTransaction,
350 + signature: signature,
351 + );
352 +
353 + return PendingTronTransaction(
354 + signedTransaction: signature,
355 + amount: totalAmount,
356 + fee: TronHelper.fromSun(rawTransaction.feeLimit ?? BigInt.zero),
357 + sendTransaction: sendTx,
358 + );
359 + }
360 +
361 + Future<TransactionRaw> _signNativeTransaction(
362 + TronAddress ownerAddress,
363 + TronAddress receiverAddress,
364 + String amount,
365 + BigInt tronBalance,
366 + bool sendAll,
367 + ) async {
368 + // This is introduce to server as a limit in cases where feeLimit is 0
369 + // The transaction signing will fail if the feeLimit is explicitly 0.
370 + int defaultFeeLimit = 100000;
371 +
372 + final block = await _provider!.request(TronRequestGetNowBlock());
373 + // Create the transfer contract
374 + final contract = TransferContract(
375 + amount: TronHelper.toSun(amount),
376 + ownerAddress: ownerAddress,
377 + toAddress: receiverAddress,
378 + );
379 +
380 + // Prepare the contract parameter for the transaction.
381 + final parameter = Any(typeUrl: contract.typeURL, value: contract);
382 +
383 + // Create a TransactionContract object with the contract type and parameter.
384 + final transactionContract =
385 + TransactionContract(type: contract.contractType, parameter: parameter);
386 +
387 + // Set the transaction expiration time (maximum 24 hours)
388 + final expireTime = DateTime.now().toUtc().add(const Duration(hours: 24));
389 +
390 + // Create a raw transaction
391 + TransactionRaw rawTransaction = TransactionRaw(
392 + refBlockBytes: block.blockHeader.rawData.refBlockBytes,
393 + refBlockHash: block.blockHeader.rawData.refBlockHash,
394 + expiration: BigInt.from(expireTime.millisecondsSinceEpoch),
395 + contract: [transactionContract],
396 + timestamp: block.blockHeader.rawData.timestamp,
397 + );
398 +
399 + final feeLimit = await getFeeLimit(rawTransaction, ownerAddress, receiverAddress);
400 + final feeLimitToUse = feeLimit != 0 ? feeLimit : defaultFeeLimit;
401 + final tronBalanceInt = tronBalance.toInt();
402 +
403 + if (feeLimit > tronBalanceInt) {
404 + throw Exception(
405 + 'You don\'t have enough TRX to cover the transaction fee for this transaction. Kindly top up.',
406 + );
407 + }
408 +
409 + rawTransaction = rawTransaction.copyWith(
410 + feeLimit: BigInt.from(feeLimitToUse),
411 + );
412 +
413 + return rawTransaction;
414 + }
415 +
416 + Future<TransactionRaw> _signTrcTokenTransaction(
417 + TronAddress ownerAddress,
418 + TronAddress receiverAddress,
419 + String amount,
420 + String contractAddress,
421 + BigInt tronBalance,
422 + ) async {
423 + final contract = ContractABI.fromJson(trc20Abi, isTron: true);
424 +
425 + final function = contract.functionFromName("transfer");
426 +
427 + /// address /// amount
428 + final transferparams = [
429 + receiverAddress,
430 + TronHelper.toSun(amount),
431 + ];
432 +
433 + final contractAddr = TronAddress(contractAddress);
434 +
435 + final request = await _provider!.request(
436 + TronRequestTriggerConstantContract(
437 + ownerAddress: ownerAddress,
438 + contractAddress: contractAddr,
439 + data: function.encodeHex(transferparams),
440 + ),
441 + );
442 +
443 + if (!request.isSuccess) {
444 + log("Tron TRC20 error: ${request.error} \n ${request.respose}");
445 + }
446 +
447 + final feeLimit = await getFeeLimit(
448 + request.transactionRaw!,
449 + ownerAddress,
450 + receiverAddress,
451 + energyUsed: request.energyUsed ?? 0,
452 + );
453 +
454 + final tronBalanceInt = tronBalance.toInt();
455 +
456 + if (feeLimit > tronBalanceInt) {
457 + throw Exception(
458 + 'You don\'t have enough TRX to cover the transaction fee for this transaction. Kindly top up.',
459 + );
460 + }
461 +
462 + final rawTransaction = request.transactionRaw!.copyWith(
463 + feeLimit: BigInt.from(feeLimit),
464 + );
465 +
466 + return rawTransaction;
467 + }
468 +
469 + Future<String> sendTransaction({
470 + required TransactionRaw rawTransaction,
471 + required List<int> signature,
472 + }) async {
473 + try {
474 + final transaction = Transaction(rawData: rawTransaction, signature: [signature]);
475 +
476 + final raw = BytesUtils.toHexString(transaction.toBuffer());
477 +
478 + final txBroadcastResult = await _provider!.request(TronRequestBroadcastHex(transaction: raw));
479 +
480 + if (txBroadcastResult.isSuccess) {
481 + return txBroadcastResult.txId!;
482 + } else {
483 + throw Exception(txBroadcastResult.error);
484 + }
485 + } catch (e) {
486 + log('Send block Exception: ${e.toString()}');
487 + throw Exception(e);
488 + }
489 + }
490 +
491 + Future<TronBalance> fetchTronTokenBalances(String userAddress, String contractAddress) async {
492 + try {
493 + final ownerAddress = TronAddress(userAddress);
494 +
495 + final tokenAddress = TronAddress(contractAddress);
496 +
497 + final contract = ContractABI.fromJson(trc20Abi, isTron: true);
498 +
499 + final function = contract.functionFromName("balanceOf");
500 +
501 + final request = await _provider!.request(
502 + TronRequestTriggerConstantContract.fromMethod(
503 + ownerAddress: ownerAddress,
504 + contractAddress: tokenAddress,
505 + function: function,
506 + params: [ownerAddress],
507 + ),
508 + );
509 +
510 + final outputResult = request.outputResult?.first ?? BigInt.zero;
511 +
512 + return TronBalance(outputResult);
513 + } catch (_) {
514 + return TronBalance(BigInt.zero);
515 + }
516 + }
517 +
518 + Future<TronToken?> getTronToken(String contractAddress, String userAddress) async {
519 + try {
520 + final tokenAddress = TronAddress(contractAddress);
521 +
522 + final ownerAddress = TronAddress(userAddress);
523 +
524 + final contract = ContractABI.fromJson(trc20Abi, isTron: true);
525 +
526 + final name =
527 + (await getTokenDetail(contract, "name", ownerAddress, tokenAddress) as String?) ?? '';
528 +
529 + final symbol =
530 + (await getTokenDetail(contract, "symbol", ownerAddress, tokenAddress) as String?) ?? '';
531 +
532 + final decimal =
533 + (await getTokenDetail(contract, "decimals", ownerAddress, tokenAddress) as BigInt?) ??
534 + BigInt.zero;
535 +
536 + return TronToken(
537 + name: name,
538 + symbol: symbol,
539 + contractAddress: contractAddress,
540 + decimal: decimal.toInt(),
541 + );
542 + } catch (e) {
543 + return null;
544 + }
545 + }
546 +
547 + Future<dynamic> getTokenDetail(
548 + ContractABI contract,
549 + String functionName,
550 + TronAddress ownerAddress,
551 + TronAddress tokenAddress,
552 + ) async {
553 + final function = contract.functionFromName(functionName);
554 +
555 + try {
556 + final request = await _provider!.request(
557 + TronRequestTriggerConstantContract.fromMethod(
558 + ownerAddress: ownerAddress,
559 + contractAddress: tokenAddress,
560 + function: function,
561 + params: [],
562 + ),
563 + );
564 +
565 + final outputResult = request.outputResult?.first;
566 +
567 + return outputResult;
568 + } catch (_) {
569 + log('Erorr fetching detail: ${_.toString()}');
570 +
571 + return null;
572 + }
573 + }
574 +}
cw_tron/lib/tron_exception.dart new
+16
@@ -0,0 +1,16 @@
1 +import 'package:cw_core/crypto_currency.dart';
2 +
3 +class TronMnemonicIsIncorrectException implements Exception {
4 + @override
5 + String toString() =>
6 + 'Tron mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.';
7 +}
8 +class TronTransactionCreationException implements Exception {
9 + final String exceptionMessage;
10 +
11 + TronTransactionCreationException(CryptoCurrency currency)
12 + : exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.';
13 +
14 + @override
15 + String toString() => exceptionMessage;
16 +}
\ No newline at end of file
cw_tron/lib/tron_http_provider.dart new
+41
@@ -0,0 +1,41 @@
1 +import 'dart:convert';
2 +
3 +import 'package:http/http.dart' as http;
4 +import 'package:on_chain/tron/tron.dart';
5 +import '.secrets.g.dart' as secrets;
6 +
7 +class TronHTTPProvider implements TronServiceProvider {
8 + TronHTTPProvider(
9 + {required this.url,
10 + http.Client? client,
11 + this.defaultRequestTimeout = const Duration(seconds: 30)})
12 + : client = client ?? http.Client();
13 + @override
14 + final String url;
15 + final http.Client client;
16 + final Duration defaultRequestTimeout;
17 +
18 + @override
19 + Future<Map<String, dynamic>> get(TronRequestDetails params, [Duration? timeout]) async {
20 + final response = await client.get(Uri.parse(params.url(url)), headers: {
21 + 'Content-Type': 'application/json',
22 + 'TRON-PRO-API-KEY': secrets.tronGridApiKey,
23 + }).timeout(timeout ?? defaultRequestTimeout);
24 + final data = json.decode(response.body) as Map<String, dynamic>;
25 + return data;
26 + }
27 +
28 + @override
29 + Future<Map<String, dynamic>> post(TronRequestDetails params, [Duration? timeout]) async {
30 + final response = await client
31 + .post(Uri.parse(params.url(url)),
32 + headers: {
33 + 'Content-Type': 'application/json',
34 + 'TRON-PRO-API-KEY': secrets.tronGridApiKey,
35 + },
36 + body: params.toRequestBody())
37 + .timeout(timeout ?? defaultRequestTimeout);
38 + final data = json.decode(response.body) as Map<String, dynamic>;
39 + return data;
40 + }
41 +}
cw_tron/lib/tron_token.dart new
+80
@@ -0,0 +1,80 @@
1 +// ignore_for_file: annotate_overrides, overridden_fields
2 +
3 +import 'package:cw_core/crypto_currency.dart';
4 +import 'package:cw_core/hive_type_ids.dart';
5 +import 'package:hive/hive.dart';
6 +
7 +part 'tron_token.g.dart';
8 +
9 +@HiveType(typeId: TronToken.typeId)
10 +class TronToken extends CryptoCurrency with HiveObjectMixin {
11 + @HiveField(0)
12 + final String name;
13 +
14 + @HiveField(1)
15 + final String symbol;
16 +
17 + @HiveField(2)
18 + final String contractAddress;
19 +
20 + @HiveField(3)
21 + final int decimal;
22 +
23 + @HiveField(4, defaultValue: true)
24 + bool _enabled;
25 +
26 + @HiveField(5)
27 + final String? iconPath;
28 +
29 + @HiveField(6)
30 + final String? tag;
31 +
32 + bool get enabled => _enabled;
33 +
34 + set enabled(bool value) => _enabled = value;
35 +
36 + TronToken({
37 + required this.name,
38 + required this.symbol,
39 + required this.contractAddress,
40 + required this.decimal,
41 + bool enabled = true,
42 + this.iconPath,
43 + this.tag = 'TRX',
44 + }) : _enabled = enabled,
45 + super(
46 + name: symbol.toLowerCase(),
47 + title: symbol.toUpperCase(),
48 + fullName: name,
49 + tag: tag,
50 + iconPath: iconPath,
51 + decimals: decimal);
52 +
53 + TronToken.copyWith(TronToken other, String? icon, String? tag)
54 + : name = other.name,
55 + symbol = other.symbol,
56 + contractAddress = other.contractAddress,
57 + decimal = other.decimal,
58 + _enabled = other.enabled,
59 + tag = tag ?? other.tag,
60 + iconPath = icon ?? other.iconPath,
61 + super(
62 + name: other.name,
63 + title: other.symbol.toUpperCase(),
64 + fullName: other.name,
65 + tag: tag ?? other.tag,
66 + iconPath: icon ?? other.iconPath,
67 + decimals: other.decimal,
68 + );
69 +
70 + static const typeId = TRON_TOKEN_TYPE_ID;
71 + static const boxName = 'TronTokens';
72 +
73 + @override
74 + bool operator ==(other) =>
75 + (other is TronToken && other.contractAddress == contractAddress) ||
76 + (other is CryptoCurrency && other.title == title);
77 +
78 + @override
79 + int get hashCode => contractAddress.hashCode;
80 +}
cw_tron/lib/tron_transaction_credentials.dart new
+12
@@ -0,0 +1,12 @@
1 +import 'package:cw_core/crypto_currency.dart';
2 +import 'package:cw_core/output_info.dart';
3 +
4 +class TronTransactionCredentials {
5 + TronTransactionCredentials(
6 + this.outputs, {
7 + required this.currency,
8 + });
9 +
10 + final List<OutputInfo> outputs;
11 + final CryptoCurrency currency;
12 +}
cw_tron/lib/tron_transaction_history.dart new
+80
@@ -0,0 +1,80 @@
1 +import 'dart:convert';
2 +import 'dart:core';
3 +import 'dart:developer';
4 +import 'package:cw_core/pathForWallet.dart';
5 +import 'package:cw_core/wallet_info.dart';
6 +import 'package:cw_evm/file.dart';
7 +import 'package:cw_tron/tron_transaction_info.dart';
8 +import 'package:mobx/mobx.dart';
9 +import 'package:cw_core/transaction_history.dart';
10 +
11 +part 'tron_transaction_history.g.dart';
12 +
13 +class TronTransactionHistory = TronTransactionHistoryBase with _$TronTransactionHistory;
14 +
15 +abstract class TronTransactionHistoryBase extends TransactionHistoryBase<TronTransactionInfo>
16 + with Store {
17 + TronTransactionHistoryBase({required this.walletInfo, required String password})
18 + : _password = password {
19 + transactions = ObservableMap<String, TronTransactionInfo>();
20 + }
21 +
22 + String _password;
23 +
24 + final WalletInfo walletInfo;
25 +
26 + Future<void> init() async => await _load();
27 +
28 + @override
29 + Future<void> save() async {
30 + String transactionsHistoryFileNameForWallet = 'tron_transactions.json';
31 + try {
32 + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
33 + String path = '$dirPath/$transactionsHistoryFileNameForWallet';
34 + final transactionMaps = transactions.map((key, value) => MapEntry(key, value.toJson()));
35 + final data = json.encode({'transactions': transactionMaps});
36 + await writeData(path: path, password: _password, data: data);
37 + } catch (e, s) {
38 + log('Error while saving ${walletInfo.type.name} transaction history: ${e.toString()}');
39 + log(s.toString());
40 + }
41 + }
42 +
43 + @override
44 + void addOne(TronTransactionInfo transaction) => transactions[transaction.id] = transaction;
45 +
46 + @override
47 + void addMany(Map<String, TronTransactionInfo> transactions) =>
48 + this.transactions.addAll(transactions);
49 +
50 + Future<Map<String, dynamic>> _read() async {
51 + String transactionsHistoryFileNameForWallet = 'tron_transactions.json';
52 + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
53 + String path = '$dirPath/$transactionsHistoryFileNameForWallet';
54 + final content = await read(path: path, password: _password);
55 + if (content.isEmpty) {
56 + return {};
57 + }
58 + return json.decode(content) as Map<String, dynamic>;
59 + }
60 +
61 + Future<void> _load() async {
62 + try {
63 + final content = await _read();
64 + final txs = content['transactions'] as Map<String, dynamic>? ?? {};
65 +
66 + for (var entry in txs.entries) {
67 + final val = entry.value;
68 +
69 + if (val is Map<String, dynamic>) {
70 + final tx = TronTransactionInfo.fromJson(val);
71 + _update(tx);
72 + }
73 + }
74 + } catch (e) {
75 + log(e.toString());
76 + }
77 + }
78 +
79 + void _update(TronTransactionInfo transaction) => transactions[transaction.id] = transaction;
80 +}
cw_tron/lib/tron_transaction_info.dart new
+93
@@ -0,0 +1,93 @@
1 +import 'package:cw_core/format_amount.dart';
2 +import 'package:cw_core/transaction_direction.dart';
3 +import 'package:cw_core/transaction_info.dart';
4 +import 'package:on_chain/on_chain.dart' as onchain;
5 +import 'package:on_chain/tron/tron.dart';
6 +
7 +class TronTransactionInfo extends TransactionInfo {
8 + TronTransactionInfo({
9 + required this.id,
10 + required this.tronAmount,
11 + required this.txFee,
12 + required this.direction,
13 + required this.blockTime,
14 + required this.to,
15 + required this.from,
16 + required this.isPending,
17 + this.tokenSymbol = 'TRX',
18 + }) : amount = tronAmount.toInt();
19 +
20 + final String id;
21 + final String? to;
22 + final String? from;
23 + final int amount;
24 + final BigInt tronAmount;
25 + final String tokenSymbol;
26 + final DateTime blockTime;
27 + final bool isPending;
28 + final int? txFee;
29 + final TransactionDirection direction;
30 +
31 + factory TronTransactionInfo.fromJson(Map<String, dynamic> data) {
32 + return TronTransactionInfo(
33 + id: data['id'] as String,
34 + tronAmount: BigInt.parse(data['tronAmount']),
35 + txFee: data['txFee'],
36 + direction: parseTransactionDirectionFromInt(data['direction'] as int),
37 + blockTime: DateTime.fromMillisecondsSinceEpoch(data['blockTime'] as int),
38 + tokenSymbol: data['tokenSymbol'] as String,
39 + to: data['to'],
40 + from: data['from'],
41 + isPending: data['isPending'],
42 + );
43 + }
44 +
45 + Map<String, dynamic> toJson() => {
46 + 'id': id,
47 + 'tronAmount': tronAmount.toString(),
48 + 'txFee': txFee,
49 + 'direction': direction.index,
50 + 'blockTime': blockTime.millisecondsSinceEpoch,
51 + 'tokenSymbol': tokenSymbol,
52 + 'to': to,
53 + 'from': from,
54 + 'isPending': isPending,
55 + };
56 +
57 + @override
58 + DateTime get date => blockTime;
59 +
60 + String? _fiatAmount;
61 +
62 + @override
63 + String amountFormatted() {
64 + String formattedAmount = _rawAmountAsString(tronAmount);
65 +
66 + return '$formattedAmount $tokenSymbol';
67 + }
68 +
69 + @override
70 + String fiatAmount() => _fiatAmount ?? '';
71 +
72 + @override
73 + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
74 +
75 + @override
76 + String feeFormatted() {
77 + final formattedFee = onchain.TronHelper.fromSun(BigInt.from(txFee ?? 0));
78 +
79 + return '$formattedFee TRX';
80 + }
81 +
82 + String _rawAmountAsString(BigInt amount) {
83 + String formattedAmount = TronHelper.fromSun(amount);
84 +
85 + if (formattedAmount.length >= 8) {
86 + formattedAmount = formattedAmount.substring(0, 8);
87 + }
88 +
89 + return formattedAmount;
90 + }
91 +
92 + String rawTronAmount() => _rawAmountAsString(tronAmount);
93 +}
cw_tron/lib/tron_transaction_model.dart new
+205
@@ -0,0 +1,205 @@
1 +import 'package:blockchain_utils/hex/hex.dart';
2 +import 'package:on_chain/on_chain.dart';
3 +
4 +class TronTRC20TransactionModel extends TronTransactionModel {
5 + String? transactionId;
6 +
7 + String? tokenSymbol;
8 +
9 + int? timestamp;
10 +
11 + @override
12 + String? from;
13 +
14 + @override
15 + String? to;
16 +
17 + String? value;
18 +
19 + @override
20 + String get hash => transactionId!;
21 +
22 + @override
23 + DateTime get date => DateTime.fromMillisecondsSinceEpoch(timestamp ?? 0);
24 +
25 + @override
26 + BigInt? get amount => BigInt.parse(value ?? '0');
27 +
28 + @override
29 + int? get fee => 0;
30 +
31 + TronTRC20TransactionModel({
32 + this.transactionId,
33 + this.tokenSymbol,
34 + this.timestamp,
35 + this.from,
36 + this.to,
37 + this.value,
38 + });
39 +
40 + TronTRC20TransactionModel.fromJson(Map<String, dynamic> json) {
41 + transactionId = json['transaction_id'];
42 + tokenSymbol = json['token_info'] != null ? json['token_info']['symbol'] : null;
43 + timestamp = json['block_timestamp'];
44 + from = json['from'];
45 + to = json['to'];
46 + value = json['value'];
47 + }
48 +}
49 +
50 +class TronTransactionModel {
51 + List<Ret>? ret;
52 + String? txID;
53 + int? blockTimestamp;
54 + List<Contract>? contracts;
55 +
56 + /// Getters to extract out the needed/useful information directly from the model params
57 + /// Without having to go through extra steps in the methods that use this model.
58 + bool get isError {
59 + if (ret?.first.contractRet == null) return true;
60 +
61 + return ret?.first.contractRet != "SUCCESS";
62 + }
63 +
64 + String get hash => txID!;
65 +
66 + DateTime get date => DateTime.fromMillisecondsSinceEpoch(blockTimestamp ?? 0);
67 +
68 + String? get from => contracts?.first.parameter?.value?.ownerAddress;
69 +
70 + String? get to => contracts?.first.parameter?.value?.receiverAddress;
71 +
72 + BigInt? get amount => contracts?.first.parameter?.value?.txAmount;
73 +
74 + int? get fee => ret?.first.fee;
75 +
76 + String? get contractAddress => contracts?.first.parameter?.value?.contractAddress;
77 +
78 + TronTransactionModel({
79 + this.ret,
80 + this.txID,
81 + this.blockTimestamp,
82 + this.contracts,
83 + });
84 +
85 + TronTransactionModel.fromJson(Map<String, dynamic> json) {
86 + if (json['ret'] != null) {
87 + ret = <Ret>[];
88 + json['ret'].forEach((v) {
89 + ret!.add(Ret.fromJson(v));
90 + });
91 + }
92 + txID = json['txID'];
93 + blockTimestamp = json['block_timestamp'];
94 + contracts = json['raw_data'] != null
95 + ? (json['raw_data']['contract'] as List)
96 + .map((e) => Contract.fromJson(e as Map<String, dynamic>))
97 + .toList()
98 + : null;
99 + }
100 +}
101 +
102 +class Ret {
103 + String? contractRet;
104 + int? fee;
105 +
106 + Ret({this.contractRet, this.fee});
107 +
108 + Ret.fromJson(Map<String, dynamic> json) {
109 + contractRet = json['contractRet'];
110 + fee = json['fee'];
111 + }
112 +}
113 +
114 +class Contract {
115 + Parameter? parameter;
116 + String? type;
117 +
118 + Contract({this.parameter, this.type});
119 +
120 + Contract.fromJson(Map<String, dynamic> json) {
121 + parameter = json['parameter'] != null ? Parameter.fromJson(json['parameter']) : null;
122 + type = json['type'];
123 + }
124 +}
125 +
126 +class Parameter {
127 + Value? value;
128 + String? typeUrl;
129 +
130 + Parameter({this.value, this.typeUrl});
131 +
132 + Parameter.fromJson(Map<String, dynamic> json) {
133 + value = json['value'] != null ? Value.fromJson(json['value']) : null;
134 + typeUrl = json['type_url'];
135 + }
136 +}
137 +
138 +class Value {
139 + String? data;
140 + String? ownerAddress;
141 + String? contractAddress;
142 + int? amount;
143 + String? toAddress;
144 + String? assetName;
145 +
146 + //Getters to extract address for tron transactions
147 + /// If the contract address is null, it returns the toAddress
148 + /// If it's not null, it decodes the data field and gets the receiver address.
149 + String? get receiverAddress {
150 + if (contractAddress == null) return toAddress;
151 +
152 + if (data == null) return null;
153 +
154 + return _decodeAddressFromEncodedDataField(data!);
155 + }
156 +
157 + //Getters to extract amount for tron transactions
158 + /// If the contract address is null, it returns the amount
159 + /// If it's not null, it decodes the data field and gets the tx amount.
160 + BigInt? get txAmount {
161 + if (contractAddress == null) return BigInt.from(amount ?? 0);
162 +
163 + if (data == null) return null;
164 +
165 + return _decodeAmountInvolvedFromEncodedDataField(data!);
166 + }
167 +
168 + Value(
169 + {this.data,
170 + this.ownerAddress,
171 + this.contractAddress,
172 + this.amount,
173 + this.toAddress,
174 + this.assetName});
175 +
176 + Value.fromJson(Map<String, dynamic> json) {
177 + data = json['data'];
178 + ownerAddress = json['owner_address'];
179 + contractAddress = json['contract_address'];
180 + amount = json['amount'];
181 + toAddress = json['to_address'];
182 + assetName = json['asset_name'];
183 + }
184 +
185 + /// To get the address from the encoded data field
186 + String _decodeAddressFromEncodedDataField(String output) {
187 + // To get the receiver address from the encoded params
188 + output = output.replaceFirst('0x', '').substring(8);
189 + final abiCoder = ABICoder.fromType('address');
190 + final decoded = abiCoder.decode(AbiParameter.bytes, hex.decode(output));
191 + final tronAddress = TronAddress.fromEthAddress((decoded.result as ETHAddress).toBytes());
192 +
193 + return tronAddress.toString();
194 + }
195 +
196 + /// To get the amount from the encoded data field
197 + BigInt _decodeAmountInvolvedFromEncodedDataField(String output) {
198 + output = output.replaceFirst('0x', '').substring(72);
199 + final amountAbiCoder = ABICoder.fromType('uint256');
200 + final decodedA = amountAbiCoder.decode(AbiParameter.uint256, hex.decode(output));
201 + final amount = decodedA.result as BigInt;
202 +
203 + return amount;
204 + }
205 +}
cw_tron/lib/tron_wallet.dart new
+560
@@ -0,0 +1,560 @@
1 +import 'dart:async';
2 +import 'dart:convert';
3 +import 'dart:developer';
4 +import 'dart:io';
5 +
6 +import 'package:bip39/bip39.dart' as bip39;
7 +import 'package:blockchain_utils/blockchain_utils.dart';
8 +import 'package:cw_core/cake_hive.dart';
9 +import 'package:cw_core/crypto_currency.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_type.dart';
20 +import 'package:cw_tron/default_tron_tokens.dart';
21 +import 'package:cw_tron/file.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_tron/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:hive/hive.dart';
32 +import 'package:mobx/mobx.dart';
33 +import 'package:on_chain/on_chain.dart';
34 +import 'package:shared_preferences/shared_preferences.dart';
35 +
36 +part 'tron_wallet.g.dart';
37 +
38 +class TronWallet = TronWalletBase with _$TronWallet;
39 +
40 +abstract class TronWalletBase
41 + extends WalletBase<TronBalance, TronTransactionHistory, TronTransactionInfo> with Store {
42 + TronWalletBase({
43 + required WalletInfo walletInfo,
44 + String? mnemonic,
45 + String? privateKey,
46 + required String password,
47 + TronBalance? initialBalance,
48 + }) : syncStatus = const NotConnectedSyncStatus(),
49 + _password = password,
50 + _mnemonic = mnemonic,
51 + _hexPrivateKey = privateKey,
52 + _client = TronClient(),
53 + walletAddresses = TronWalletAddresses(walletInfo),
54 + balance = ObservableMap<CryptoCurrency, TronBalance>.of(
55 + {CryptoCurrency.trx: initialBalance ?? TronBalance(BigInt.zero)},
56 + ),
57 + super(walletInfo) {
58 + this.walletInfo = walletInfo;
59 + transactionHistory = TronTransactionHistory(walletInfo: walletInfo, password: password);
60 +
61 + if (!CakeHive.isAdapterRegistered(TronToken.typeId)) {
62 + CakeHive.registerAdapter(TronTokenAdapter());
63 + }
64 +
65 + sharedPrefs.complete(SharedPreferences.getInstance());
66 + }
67 +
68 + final String? _mnemonic;
69 + final String? _hexPrivateKey;
70 + final String _password;
71 +
72 + late final Box<TronToken> tronTokensBox;
73 +
74 + late final TronPrivateKey _tronPrivateKey;
75 +
76 + late final TronPublicKey _tronPublicKey;
77 +
78 + TronPublicKey get tronPublicKey => _tronPublicKey;
79 +
80 + TronPrivateKey get tronPrivateKey => _tronPrivateKey;
81 +
82 + late String _tronAddress;
83 +
84 + late TronClient _client;
85 +
86 + Timer? _transactionsUpdateTimer;
87 +
88 + @override
89 + WalletAddresses walletAddresses;
90 +
91 + @observable
92 + String? nativeTxEstimatedFee;
93 +
94 + @observable
95 + String? trc20EstimatedFee;
96 +
97 + @override
98 + @observable
99 + SyncStatus syncStatus;
100 +
101 + @override
102 + @observable
103 + late ObservableMap<CryptoCurrency, TronBalance> balance;
104 +
105 + Completer<SharedPreferences> sharedPrefs = Completer();
106 +
107 + Future<void> init() async {
108 + await initTronTokensBox();
109 +
110 + await walletAddresses.init();
111 + await transactionHistory.init();
112 + _tronPrivateKey = await getPrivateKey(
113 + mnemonic: _mnemonic,
114 + privateKey: _hexPrivateKey,
115 + password: _password,
116 + );
117 +
118 + _tronPublicKey = _tronPrivateKey.publicKey();
119 +
120 + _tronAddress = _tronPublicKey.toAddress().toString();
121 +
122 + walletAddresses.address = _tronAddress;
123 +
124 + await save();
125 + }
126 +
127 + static Future<TronWallet> open({
128 + required String name,
129 + required String password,
130 + required WalletInfo walletInfo,
131 + }) async {
132 + final path = await pathForWallet(name: name, type: walletInfo.type);
133 + final jsonSource = await read(path: path, password: password);
134 + final data = json.decode(jsonSource) as Map;
135 + final mnemonic = data['mnemonic'] as String?;
136 + final privateKey = data['private_key'] as String?;
137 + final balance = TronBalance.fromJSON(data['balance'] as String) ?? TronBalance(BigInt.zero);
138 +
139 + return TronWallet(
140 + walletInfo: walletInfo,
141 + password: password,
142 + mnemonic: mnemonic,
143 + privateKey: privateKey,
144 + initialBalance: balance,
145 + );
146 + }
147 +
148 + void addInitialTokens() {
149 + final initialTronTokens = DefaultTronTokens().initialTronTokens;
150 +
151 + for (var token in initialTronTokens) {
152 + tronTokensBox.put(token.contractAddress, token);
153 + }
154 + }
155 +
156 + Future<void> initTronTokensBox() async {
157 + final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${TronToken.boxName}";
158 +
159 + tronTokensBox = await CakeHive.openBox<TronToken>(boxName);
160 + }
161 +
162 + String idFor(String name, WalletType type) => '${walletTypeToString(type).toLowerCase()}_$name';
163 +
164 + Future<TronPrivateKey> getPrivateKey({
165 + String? mnemonic,
166 + String? privateKey,
167 + required String password,
168 + }) async {
169 + assert(mnemonic != null || privateKey != null);
170 +
171 + if (privateKey != null) {
172 + return TronPrivateKey(privateKey);
173 + }
174 +
175 + final seed = bip39.mnemonicToSeed(mnemonic!);
176 +
177 + // Derive a TRON private key from the seed
178 + final bip44 = Bip44.fromSeed(seed, Bip44Coins.tron);
179 +
180 + final childKey = bip44.deriveDefaultPath;
181 +
182 + return TronPrivateKey.fromBytes(childKey.privateKey.raw);
183 + }
184 +
185 + @override
186 + int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0;
187 +
188 + @override
189 + Future<void> changePassword(String password) {
190 + throw UnimplementedError("changePassword");
191 + }
192 +
193 + @override
194 + void close() {
195 + _transactionsUpdateTimer?.cancel();
196 + }
197 +
198 + @action
199 + @override
200 + Future<void> connectToNode({required Node node}) async {
201 + try {
202 + syncStatus = ConnectingSyncStatus();
203 +
204 + final isConnected = _client.connect(node);
205 +
206 + if (!isConnected) {
207 + throw Exception("${walletInfo.type.name.toUpperCase()} Node connection failed");
208 + }
209 +
210 + _getEstimatedFees();
211 + _setTransactionUpdateTimer();
212 +
213 + syncStatus = ConnectedSyncStatus();
214 + } catch (e) {
215 + syncStatus = FailedSyncStatus();
216 + }
217 + }
218 +
219 + Future<void> _getEstimatedFees() async {
220 + final nativeFee = await _getNativeTxFee();
221 + nativeTxEstimatedFee = TronHelper.fromSun(BigInt.from(nativeFee));
222 +
223 + final trc20Fee = await _getTrc20TxFee();
224 + trc20EstimatedFee = TronHelper.fromSun(BigInt.from(trc20Fee));
225 +
226 + log('Native Estimated Fee: $nativeTxEstimatedFee');
227 + log('TRC20 Estimated Fee: $trc20EstimatedFee');
228 + }
229 +
230 + Future<int> _getNativeTxFee() async {
231 + try {
232 + final fee = await _client.getEstimatedFee(_tronPublicKey.toAddress());
233 + return fee;
234 + } catch (e) {
235 + log(e.toString());
236 + return 0;
237 + }
238 + }
239 +
240 + Future<int> _getTrc20TxFee() async {
241 + try {
242 + final trc20fee = await _client.getTRCEstimatedFee(_tronPublicKey.toAddress());
243 + return trc20fee;
244 + } catch (e) {
245 + log(e.toString());
246 + return 0;
247 + }
248 + }
249 +
250 + @action
251 + @override
252 + Future<void> startSync() async {
253 + try {
254 + syncStatus = AttemptingSyncStatus();
255 + await _updateBalance();
256 + await fetchTransactions();
257 + fetchTrc20ExcludedTransactions();
258 +
259 + syncStatus = SyncedSyncStatus();
260 + } catch (e) {
261 + syncStatus = FailedSyncStatus();
262 + }
263 + }
264 +
265 + @override
266 + Future<PendingTransaction> createTransaction(Object credentials) async {
267 + final tronCredentials = credentials as TronTransactionCredentials;
268 +
269 + final outputs = tronCredentials.outputs;
270 +
271 + final hasMultiDestination = outputs.length > 1;
272 +
273 + final CryptoCurrency transactionCurrency =
274 + balance.keys.firstWhere((element) => element.title == tronCredentials.currency.title);
275 +
276 + final walletBalanceForCurrency = balance[transactionCurrency]!.balance;
277 +
278 + BigInt totalAmount = BigInt.zero;
279 + bool shouldSendAll = false;
280 + if (hasMultiDestination) {
281 + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
282 + throw TronTransactionCreationException(transactionCurrency);
283 + }
284 +
285 + final totalAmountFromCredentials =
286 + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0));
287 +
288 + totalAmount = BigInt.from(totalAmountFromCredentials);
289 +
290 + if (walletBalanceForCurrency < totalAmount) {
291 + throw TronTransactionCreationException(transactionCurrency);
292 + }
293 + } else {
294 + final output = outputs.first;
295 +
296 + shouldSendAll = output.sendAll;
297 +
298 + if (shouldSendAll) {
299 + totalAmount = walletBalanceForCurrency;
300 + } else {
301 + final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0');
302 + totalAmount = TronHelper.toSun(totalOriginalAmount.toString());
303 + }
304 +
305 + if (walletBalanceForCurrency < totalAmount || totalAmount < BigInt.zero) {
306 + throw TronTransactionCreationException(transactionCurrency);
307 + }
308 + }
309 +
310 + final tronBalance = balance[CryptoCurrency.trx]?.balance ?? BigInt.zero;
311 +
312 + final pendingTransaction = await _client.signTransaction(
313 + ownerPrivKey: _tronPrivateKey,
314 + toAddress: tronCredentials.outputs.first.isParsedAddress
315 + ? tronCredentials.outputs.first.extractedAddress!
316 + : tronCredentials.outputs.first.address,
317 + amount: TronHelper.fromSun(totalAmount),
318 + currency: transactionCurrency,
319 + tronBalance: tronBalance,
320 + sendAll: shouldSendAll,
321 + );
322 +
323 + return pendingTransaction;
324 + }
325 +
326 + @override
327 + Future<Map<String, TronTransactionInfo>> fetchTransactions() async {
328 + final address = _tronAddress;
329 +
330 + final transactions = await _client.fetchTransactions(address);
331 +
332 + final Map<String, TronTransactionInfo> result = {};
333 +
334 + final contract = ContractABI.fromJson(trc20Abi, isTron: true);
335 +
336 + final ownerAddress = TronAddress(_tronAddress);
337 +
338 + for (var transactionModel in transactions) {
339 + if (transactionModel.isError) {
340 + continue;
341 + }
342 +
343 + String? tokenSymbol;
344 + if (transactionModel.contractAddress != null) {
345 + final tokenAddress = TronAddress(transactionModel.contractAddress!);
346 +
347 + tokenSymbol = (await _client.getTokenDetail(
348 + contract,
349 + "symbol",
350 + ownerAddress,
351 + tokenAddress,
352 + ) as String?) ??
353 + '';
354 + }
355 +
356 + result[transactionModel.hash] = TronTransactionInfo(
357 + id: transactionModel.hash,
358 + tronAmount: transactionModel.amount ?? BigInt.zero,
359 + direction: TronAddress(transactionModel.from!, visible: false).toAddress() == address
360 + ? TransactionDirection.outgoing
361 + : TransactionDirection.incoming,
362 + blockTime: transactionModel.date,
363 + txFee: transactionModel.fee,
364 + tokenSymbol: tokenSymbol ?? "TRX",
365 + to: transactionModel.to,
366 + from: transactionModel.from,
367 + isPending: false,
368 + );
369 + }
370 +
371 + transactionHistory.addMany(result);
372 +
373 + await transactionHistory.save();
374 +
375 + return transactionHistory.transactions;
376 + }
377 +
378 + Future<void> fetchTrc20ExcludedTransactions() async {
379 + final address = _tronAddress;
380 +
381 + final transactions = await _client.fetchTrc20ExcludedTransactions(address);
382 +
383 + final Map<String, TronTransactionInfo> result = {};
384 +
385 + for (var transactionModel in transactions) {
386 + if (transactionHistory.transactions.containsKey(transactionModel.hash)) {
387 + continue;
388 + }
389 +
390 + result[transactionModel.hash] = TronTransactionInfo(
391 + id: transactionModel.hash,
392 + tronAmount: transactionModel.amount ?? BigInt.zero,
393 + direction: transactionModel.from! == address
394 + ? TransactionDirection.outgoing
395 + : TransactionDirection.incoming,
396 + blockTime: transactionModel.date,
397 + txFee: transactionModel.fee,
398 + tokenSymbol: transactionModel.tokenSymbol ?? "TRX",
399 + to: transactionModel.to,
400 + from: transactionModel.from,
401 + isPending: false,
402 + );
403 + }
404 +
405 + transactionHistory.addMany(result);
406 +
407 + await transactionHistory.save();
408 + }
409 +
410 + @override
411 + Object get keys => throw UnimplementedError("keys");
412 +
413 + @override
414 + Future<void> rescan({required int height}) {
415 + throw UnimplementedError("rescan");
416 + }
417 +
418 + @override
419 + Future<void> save() async {
420 + await walletAddresses.updateAddressesInBox();
421 + final path = await makePath();
422 + await write(path: path, password: _password, data: toJSON());
423 + await transactionHistory.save();
424 + }
425 +
426 + @override
427 + String? get seed => _mnemonic;
428 +
429 + @override
430 + String get privateKey => _tronPrivateKey.toHex();
431 +
432 + Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
433 +
434 + String toJSON() => json.encode({
435 + 'mnemonic': _mnemonic,
436 + 'private_key': privateKey,
437 + 'balance': balance[currency]!.toJSON(),
438 + });
439 +
440 + Future<void> _updateBalance() async {
441 + balance[currency] = await _fetchTronBalance();
442 +
443 + await _fetchTronTokenBalances();
444 + await save();
445 + }
446 +
447 + Future<TronBalance> _fetchTronBalance() async {
448 + final balance = await _client.getBalance(_tronPublicKey.toAddress());
449 + return TronBalance(balance);
450 + }
451 +
452 + Future<void> _fetchTronTokenBalances() async {
453 + for (var token in tronTokensBox.values) {
454 + try {
455 + if (token.enabled) {
456 + balance[token] = await _client.fetchTronTokenBalances(
457 + _tronAddress,
458 + token.contractAddress,
459 + );
460 + } else {
461 + balance.remove(token);
462 + }
463 + } catch (_) {}
464 + }
465 + }
466 +
467 + Future<void>? updateBalance() async => await _updateBalance();
468 +
469 + List<TronToken> get tronTokenCurrencies => tronTokensBox.values.toList();
470 +
471 + Future<void> addTronToken(TronToken token) async {
472 + String? iconPath;
473 + try {
474 + iconPath = CryptoCurrency.all
475 + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
476 + .iconPath;
477 + } catch (_) {}
478 +
479 + final newToken = TronToken(
480 + name: token.name,
481 + symbol: token.symbol,
482 + contractAddress: token.contractAddress,
483 + decimal: token.decimal,
484 + enabled: token.enabled,
485 + tag: token.tag ?? "TRX",
486 + iconPath: iconPath,
487 + );
488 +
489 + await tronTokensBox.put(newToken.contractAddress, newToken);
490 +
491 + if (newToken.enabled) {
492 + balance[newToken] = await _client.fetchTronTokenBalances(
493 + _tronAddress,
494 + newToken.contractAddress,
495 + );
496 + } else {
497 + balance.remove(newToken);
498 + }
499 + }
500 +
501 + Future<void> deleteTronToken(TronToken token) async {
502 + await token.delete();
503 +
504 + balance.remove(token);
505 + await _removeTokenTransactionsInHistory(token);
506 + _updateBalance();
507 + }
508 +
509 + Future<void> _removeTokenTransactionsInHistory(TronToken token) async {
510 + transactionHistory.transactions.removeWhere((key, value) => value.tokenSymbol == token.title);
511 + await transactionHistory.save();
512 + }
513 +
514 + Future<TronToken?> getTronToken(String contractAddress) async =>
515 + await _client.getTronToken(contractAddress, _tronAddress);
516 +
517 + @override
518 + Future<void> renameWalletFiles(String newWalletName) async {
519 + String transactionHistoryFileNameForWallet = 'tron_transactions.json';
520 +
521 + final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
522 + final currentWalletFile = File(currentWalletPath);
523 +
524 + final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
525 + final currentTransactionsFile = File('$currentDirPath/$transactionHistoryFileNameForWallet');
526 +
527 + // Copies current wallet files into new wallet name's dir and files
528 + if (currentWalletFile.existsSync()) {
529 + final newWalletPath = await pathForWallet(name: newWalletName, type: type);
530 + await currentWalletFile.copy(newWalletPath);
531 + }
532 + if (currentTransactionsFile.existsSync()) {
533 + final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
534 + await currentTransactionsFile.copy('$newDirPath/$transactionHistoryFileNameForWallet');
535 + }
536 +
537 + // Delete old name's dir and files
538 + await Directory(currentDirPath).delete(recursive: true);
539 + }
540 +
541 + void _setTransactionUpdateTimer() {
542 + if (_transactionsUpdateTimer?.isActive ?? false) {
543 + _transactionsUpdateTimer!.cancel();
544 + }
545 +
546 + _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) async {
547 + _updateBalance();
548 + await fetchTransactions();
549 + fetchTrc20ExcludedTransactions();
550 + });
551 + }
552 +
553 + @override
554 + String signMessage(String message, {String? address}) =>
555 + _tronPrivateKey.signPersonalMessage(ascii.encode(message));
556 +
557 + String getTronBase58AddressFromHex(String hexAddress) {
558 + return TronAddress(hexAddress).toAddress();
559 + }
560 +}
cw_tron/lib/tron_wallet_addresses.dart new
+36
@@ -0,0 +1,36 @@
1 +import 'dart:developer';
2 +
3 +import 'package:cw_core/wallet_addresses.dart';
4 +import 'package:cw_core/wallet_info.dart';
5 +import 'package:mobx/mobx.dart';
6 +
7 +part 'tron_wallet_addresses.g.dart';
8 +
9 +class TronWalletAddresses = TronWalletAddressesBase with _$TronWalletAddresses;
10 +
11 +abstract class TronWalletAddressesBase extends WalletAddresses with Store {
12 + TronWalletAddressesBase(WalletInfo walletInfo)
13 + : address = '',
14 + super(walletInfo);
15 +
16 + @override
17 + @observable
18 + String address;
19 +
20 + @override
21 + Future<void> init() async {
22 + address = walletInfo.address;
23 + await updateAddressesInBox();
24 + }
25 +
26 + @override
27 + Future<void> updateAddressesInBox() async {
28 + try {
29 + addressesMap.clear();
30 + addressesMap[address] = '';
31 + await saveAddressesInBox();
32 + } catch (e) {
33 + log(e.toString());
34 + }
35 + }
36 +}
cw_tron/lib/tron_wallet_creation_credentials.dart new
+29
@@ -0,0 +1,29 @@
1 +import 'package:cw_core/wallet_credentials.dart';
2 +import 'package:cw_core/wallet_info.dart';
3 +
4 +class TronNewWalletCredentials extends WalletCredentials {
5 + TronNewWalletCredentials({required String name, WalletInfo? walletInfo})
6 + : super(name: name, walletInfo: walletInfo);
7 +}
8 +
9 +class TronRestoreWalletFromSeedCredentials extends WalletCredentials {
10 + TronRestoreWalletFromSeedCredentials(
11 + {required String name,
12 + required String password,
13 + required this.mnemonic,
14 + WalletInfo? walletInfo})
15 + : super(name: name, password: password, walletInfo: walletInfo);
16 +
17 + final String mnemonic;
18 +}
19 +
20 +class TronRestoreWalletFromPrivateKey extends WalletCredentials {
21 + TronRestoreWalletFromPrivateKey(
22 + {required String name,
23 + required String password,
24 + required this.privateKey,
25 + WalletInfo? walletInfo})
26 + : super(name: name, password: password, walletInfo: walletInfo);
27 +
28 + final String privateKey;
29 +}
cw_tron/lib/tron_wallet_service.dart new
+148
@@ -0,0 +1,148 @@
1 +import 'dart:io';
2 +
3 +import 'package:bip39/bip39.dart' as bip39;
4 +import 'package:cw_core/pathForWallet.dart';
5 +import 'package:cw_core/wallet_base.dart';
6 +import 'package:cw_core/wallet_info.dart';
7 +import 'package:cw_core/wallet_service.dart';
8 +import 'package:cw_core/wallet_type.dart';
9 +import 'package:cw_tron/tron_client.dart';
10 +import 'package:cw_tron/tron_exception.dart';
11 +import 'package:cw_tron/tron_wallet.dart';
12 +import 'package:cw_tron/tron_wallet_creation_credentials.dart';
13 +import 'package:hive/hive.dart';
14 +import 'package:collection/collection.dart';
15 +
16 +class TronWalletService extends WalletService<TronNewWalletCredentials,
17 + TronRestoreWalletFromSeedCredentials, TronRestoreWalletFromPrivateKey> {
18 + TronWalletService(this.walletInfoSource, {required this.client});
19 +
20 + late TronClient client;
21 +
22 + final Box<WalletInfo> walletInfoSource;
23 +
24 + @override
25 + WalletType getType() => WalletType.tron;
26 +
27 + @override
28 + Future<TronWallet> create(
29 + TronNewWalletCredentials credentials, {
30 + bool? isTestnet,
31 + }) async {
32 + final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
33 +
34 + final mnemonic = bip39.generateMnemonic(strength: strength);
35 +
36 + final wallet = TronWallet(
37 + walletInfo: credentials.walletInfo!,
38 + mnemonic: mnemonic,
39 + password: credentials.password!,
40 + );
41 +
42 + await wallet.init();
43 + wallet.addInitialTokens();
44 + await wallet.save();
45 +
46 + return wallet;
47 + }
48 +
49 + @override
50 + Future<TronWallet> openWallet(String name, String password) async {
51 + final walletInfo =
52 + walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
53 +
54 + try {
55 + final wallet = await TronWalletBase.open(
56 + name: name,
57 + password: password,
58 + walletInfo: walletInfo,
59 + );
60 +
61 + await wallet.init();
62 + await wallet.save();
63 + saveBackup(name);
64 + return wallet;
65 + } catch (_) {
66 + await restoreWalletFilesFromBackup(name);
67 +
68 + final wallet = await TronWalletBase.open(
69 + name: name,
70 + password: password,
71 + walletInfo: walletInfo,
72 + );
73 +
74 + await wallet.init();
75 + await wallet.save();
76 + return wallet;
77 + }
78 + }
79 +
80 + @override
81 + Future<TronWallet> restoreFromKeys(
82 + TronRestoreWalletFromPrivateKey credentials, {
83 + bool? isTestnet,
84 + }) async {
85 + final wallet = TronWallet(
86 + password: credentials.password!,
87 + privateKey: credentials.privateKey,
88 + walletInfo: credentials.walletInfo!,
89 + );
90 +
91 + await wallet.init();
92 + wallet.addInitialTokens();
93 + await wallet.save();
94 +
95 + return wallet;
96 + }
97 +
98 + @override
99 + Future<TronWallet> restoreFromSeed(
100 + TronRestoreWalletFromSeedCredentials credentials, {
101 + bool? isTestnet,
102 + }) async {
103 + if (!bip39.validateMnemonic(credentials.mnemonic)) {
104 + throw TronMnemonicIsIncorrectException();
105 + }
106 +
107 + final wallet = TronWallet(
108 + password: credentials.password!,
109 + mnemonic: credentials.mnemonic,
110 + walletInfo: credentials.walletInfo!,
111 + );
112 +
113 + await wallet.init();
114 + wallet.addInitialTokens();
115 + await wallet.save();
116 +
117 + return wallet;
118 + }
119 +
120 + @override
121 + Future<void> rename(String currentName, String password, String newName) async {
122 + final currentWalletInfo = walletInfoSource.values
123 + .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType()));
124 + final currentWallet = await TronWalletBase.open(
125 + password: password, name: currentName, walletInfo: currentWalletInfo);
126 +
127 + await currentWallet.renameWalletFiles(newName);
128 + await saveBackup(newName);
129 +
130 + final newWalletInfo = currentWalletInfo;
131 + newWalletInfo.id = WalletBase.idFor(newName, getType());
132 + newWalletInfo.name = newName;
133 +
134 + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo);
135 + }
136 +
137 + @override
138 + Future<bool> isWalletExit(String name) async =>
139 + File(await pathForWallet(name: name, type: getType())).existsSync();
140 +
141 + @override
142 + Future<void> remove(String wallet) async {
143 + File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
144 + final walletInfo = walletInfoSource.values
145 + .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
146 + await walletInfoSource.delete(walletInfo.key);
147 + }
148 +}
cw_tron/pubspec.yaml new
+33
@@ -0,0 +1,33 @@
1 +name: cw_tron
2 +description: A new Flutter package project.
3 +version: 0.0.1
4 +publish_to: none
5 +homepage: https://cakewallet.com
6 +
7 +environment:
8 + sdk: '>=3.0.6 <4.0.0'
9 + flutter: ">=1.17.0"
10 +
11 +dependencies:
12 + flutter:
13 + sdk: flutter
14 + cw_core:
15 + path: ../cw_core
16 + cw_evm:
17 + path: ../cw_evm
18 + on_chain: ^3.0.1
19 + blockchain_utils: ^2.1.1
20 + mobx: ^2.3.0+1
21 + bip39: ^1.0.6
22 + hive: ^2.2.3
23 +
24 +dev_dependencies:
25 + flutter_test:
26 + sdk: flutter
27 + flutter_lints: ^2.0.0
28 + build_runner: ^2.3.3
29 + mobx_codegen: ^2.1.1
30 + hive_generator: ^1.1.3
31 +flutter:
32 + # assets:
33 + # - images/a_dot_burr.jpeg
cw_tron/test/cw_tron_test.dart new
+12
@@ -0,0 +1,12 @@
1 +import 'package:flutter_test/flutter_test.dart';
2 +
3 +import 'package:cw_tron/cw_tron.dart';
4 +
5 +void main() {
6 + test('adds one to input values', () {
7 + final calculator = Calculator();
8 + expect(calculator.addOne(2), 3);
9 + expect(calculator.addOne(-7), -6);
10 + expect(calculator.addOne(0), 1);
11 + });
12 +}
how_to_add_new_wallet_type.md new
+300
@@ -0,0 +1,300 @@
1 +# Guide to adding a new wallet type in Cake Wallet
2 +
3 +## Wallet Integration
4 +
5 +**N:B** Throughout this guide, `walletx` refers to the specific wallet type you want to add. If you're adding `BNB` to CakeWallet, then `walletx` for you here is `bnb`.
6 +
7 +**Core Folder/Files Setup**
8 +- Idenitify your core component/package (major project component), which would power the integration e.g web3dart, solana, onchain etc
9 +- Add a new entry to `WalletType` class in `cw_core/wallet_type.dart`.
10 +- Fill out the necessary information int he various functions in the files, concerning the wallet name, the native currency type, symbol etc.
11 +- Go to `cw_core/lib/currency_for_wallet_type.dart`, in the `currencyForWalletType` function, add a case for `walletx`, returning the native cryptocurrency for `walletx`.
12 +- If the cryptocurrency for walletx is not available among the default cryptocurrencies, add a new cryptocurrency entry in `cw_core/lib/cryptocurrency.dart`.
13 +- Add the newly created cryptocurrency name to the list named `all` in this file.
14 +- Create a package for the wallet specific integration, name it. `cw_walletx`
15 +- Add the following initial common files and replicate to fit the wallet
16 + - walletx_transaction_history.dart
17 + - walletx_transaction_info.dart
18 + - walletx_mnemonics_exception.dart
19 + - walletx_tokens.dart
20 + - walletx_wallet_service.dart:
21 + - walletx_wallet.dart
22 + - etc.
23 +
24 +- Add the code to run the code generation needed for the files in the `cw_walletx` package to the `model_generator.sh` script
25 +
26 + cd cw_walletx && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
27 +
28 +- Add the relevant dev_dependencies for generating the files also
29 + - build_runner
30 + - mobx_codegen
31 + - hive_generator
32 +
33 +**WalletX Proxy Setup**
34 +
35 +A `Proxy` class is used to communicate with the specific wallet package we have. Instead of directly making use of methods and parameters in `cw_walletx` within the `lib` directory, we use a proxy to access these data. All important functions, calls and interactions we want to make with our `cw_walletx` package would be defined and done through the proxy class. The class would define the import
36 +
37 +- Create a proxy folder titled `walletx` to handle the wallet operations. It would contain 2 files: `cw_walletx.dart` and `walletx.dart`.
38 +- `cw_walletx.dart` file would hold an implementation class containing major operations to be done in the lib directory. It serves as the link between the cw_walletx package and the rest of the codebase(lib directory files and folders).
39 +- `walletx.dart` would contain the abstract class highlighting the methods that would bring the functionalities and features in the `cw_walletx` package to the rest of the `lib` directory.
40 +- Add `walletx.dart` to `.gitignore` as we won’t be pushing it: `lib/tron/tron.dart`.
41 +- `walletx.dart` would always be generated based on the configure files we would be setting up in the next step.
42 +
43 +**Configuration Files Setup**
44 +- Before we populate the field, head over to `tool/configure.dart` to setup the necessary configurations for the `walletx` proxy.
45 +- Define the output path, it’ll follow the format `lib/walletx/walletx.dart`.
46 +- Add the variable to check if `walletx` is to be activated
47 +- Define the function that would generate the abstract class for the proxy.(We will flesh out this function in the next steps).
48 +- Add the defined variable in step 2 to the `generatePubspec` and `generateWalletTypes`.
49 +- Next, modify the following functions:
50 + - generatePubspec function
51 + 1. Add the parameters to the method params (i.e required bool hasWalletX)
52 + 2. Define a variable to hold the entry for the pubspec.yaml file
53 +
54 + const cwWalletX = """
55 + cw_tron:
56 + path: ./cw_walletx
57 + """;
58 +
59 + 3. Add an if block that takes in the passed parameter and adds the defined variable(inn the previous step) to the list of outputs
60 +
61 + if (hasWalletX) {
62 + output += '\n$cwWalletX’;
63 + }
64 +
65 + - generateWalletTypes function
66 + 1. Add the parameters to the method params (i.e required bool hasWalletX)
67 + 2. Add an if block to add the wallet type to the list of outputs this function generates
68 +
69 + if (hasWalletX) {
70 + outputContent += '\tWalletType.walletx,\n’;
71 + }
72 +
73 +- Head over to `scripts/android/pubspec.sh` script, and modify the `CONFIG_ARGS` under `$CAKEWALLET`. Add `"—walletx”` to the end of the passed in params.
74 +- Repeat this in `scripts/ios/app_config.sh` and `scripts/macos/app_config.sh`
75 +- Open a terminal and cd into `scripts/android/`. Run the following commands to run setup configuration scripts(proxy class, add walletx to list of wallet types and add cw_walletx to pubspec).
76 +
77 + source ./app_env.sh cakewallet
78 +
79 + ./app_config.sh
80 +
81 + cd cw_walletx && flutter pub get && flutter packages pub run build_runner build
82 +
83 + flutter packages pub run build_runner build --delete-conflicting-outputs
84 +
85 +Moving forward, our interactions with the cw_walletx package would be through the proxy class and its methods.
86 +
87 +**Pre-Wallet Creation for WalletX**
88 +- Go to `di.dart` and locate the block to `registerWalletService`. In this, add the case to handle creating the WalletXWalletService
89 +
90 + case WalletType.walletx:
91 + return walletx!.createWalletXWalletService(_walletInfoSource);
92 +
93 +- Go to `lib/view_model/wallet_new_vm.dart`, in the getCredentials method, which gets the new wallet credentials for walletX add the case for the new wallet
94 +
95 + case WalletType.walletx:
96 + return walletx!.createWalletXNewWalletCredentials(name: name);
97 +
98 +**Node Setup**
99 +- Before we can be able to successfully create a new wallet of wallet type walletx we need to setup the node that the wallet would use:
100 +- In the assets directory, create a new file and name it `walletx_node_list.yml`. This yml file would contain the details for nodes to be used for walletX. An example structure for each node entry
101 +
102 + uri: "api.nodeurl.io"
103 + is_default: true
104 + useSSL: true
105 +
106 +You can add as many node entries as desired.
107 +
108 +- Add the path to the yml file created to the `pubspec_base.yaml` file (`“assets/walletx_node_list.yml”`)
109 +- Go to `lib/entities/node_list.dart`, add a function to load the node entries we made in `walletx_node_list.yml` for walletx.
110 +- Name your function `loadDefaultWalletXNodes()`. The function would handle loading the yml file as a string and parsing it into a Node Object to be used within the app. Here’s a template for the function.
111 +
112 + Future<List<Node>> loadDefaultWalletXNodes() async {
113 + final nodesRaw = await rootBundle.loadString('assets/tron_node_list.yml');
114 + final loadedNodes = loadYaml(nodesRaw) as YamlList;
115 + final nodes = <Node>[];
116 + for (final raw in loadedNodes) {
117 + if (raw is Map) {
118 + final node = Node.fromMap(Map<String, Object>.from(raw));
119 + node.type = WalletType.tron;
120 + nodes.add(node);
121 + }
122 + }
123 + return nodes;
124 + }
125 +
126 +- Inside the `resetToDefault` function, call the function you created and add the result to the nodes result variable.
127 +- Go to `lib/entities/default_settings_migration.dart` file, we’ll be adding the following to the file.
128 +- At the top of the file, after the imports, define the default nodeUrl for wallet-name.
129 +- Next, write a function to fetch the node for this default uri you added above.
130 +
131 + Node? getWalletXDefaultNode({required Box<Node> nodes}) {
132 + return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == walletXDefaultNodeUri) ??
133 + nodes.values.firstWhereOrNull((node) => node.type == WalletType.walletx);
134 + }
135 +
136 +- Next, write a function that will add the list of nodes we declared in the `walletx_node_list.yml` file to the Nodes Box, to be used in the app. Here’s the format for this function
137 +
138 + Future<void> addWalletXNodeList({required Box<Node> nodes}) async {
139 + final nodeList = await loadDefaultWalletXNodes();
140 + for (var node in nodeList) {
141 + if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) {
142 + await nodes.add(node);
143 + }
144 + }
145 + }
146 +
147 +- Next, we’ll write the function to change walletX current node to default. An handy function we would make use of later on. Add a new preference key in `lib/entities/preference_key.dart` with the format `PreferencesKey.currentWalletXNodeIdKey`, we’ll use it to identify the current node id.
148 +
149 + Future<void> changeWalletXCurrentNodeToDefault(
150 + {required SharedPreferences sharedPreferences, required Box<Node> nodes}) async {
151 + final node = getWalletXDefaultNode(nodes: nodes);
152 + final nodeId = node?.key as int? ?? 0;
153 + await sharedPreferences.setInt(PreferencesKey.currentWalletXNodeIdKey, nodeId);
154 + }
155 +
156 +- Next, in the `defaultSettingsMigration` function at the top of the file, add a new case to handle both `addWalletXNodeList` and `changeWalletXCurrentNodeToDefault`
157 +
158 + case “next-number-increment”:
159 + await addWalletXNodeList(nodes: nodes);
160 + await changeWalletXCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes);
161 + break;
162 +
163 +- Next, increase the `initialMigrationVersion` number in `main.dart` to be the new case entry number you entered in the step above for the `defaultSettingsMigration` function.
164 +- Next, go to `lib/view_model/node_list/node_list_view_model.dart`
165 +- In the `reset` function, add a case for walletX:
166 +
167 + case WalletType.tron:
168 + node = getTronDefaultNode(nodes: _nodeSource)!;
169 + break;
170 +
171 +- Lastly, go to `cw_core/lib/node.dart`,
172 +- In the uri getter, add a case to handle the uri setup for walletX. If the node uses http, return `Uri.http`, if not, return `Uri.https`
173 +
174 + case WalletType.walletX:
175 + return Uri.https(uriRaw, ‘’);
176 +
177 +- Also, in the `requestNode` method, add a case for `WalletType.walletx`
178 +- Next is the modifications to `lib/store/settings_store.dart` file:
179 +- In the `load` function, create a variable to fetch the currentWalletxNodeId using the `PreferencesKey.currentWalletXNodeIdKey` we created earlier.
180 +- Create another variable `walletXNode` which gets the walletx node using the nodeId variable assigned in the step above.
181 +- Add a check to see if walletXNode is not null, if it’s not null, assign the created tronNode variable to the nodeMap with a type of walletX
182 +
183 + final walletXNode = nodeSource.get(walletXNodeId);
184 + final walletXNodeId = sharedPreferences.getInt(PreferencesKey.currentWalletXNodeIdKey);
185 + if (walletXNode != null) {
186 + nodes[WalletType.walletx] = walletXNode;
187 + }
188 +
189 +- Repeat the steps above in the `reload` function
190 +- Next, add a case for walletX in the `_saveCurrentNode` function.
191 +
192 +- Run the following commands after to generate modified files in cw_core and lib
193 +
194 + cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
195 +
196 + flutter packages pub run build_runner build --delete-conflicting-outputs
197 +
198 +- Lastly, before we run the app to test what we’ve done so far,
199 +- Go to `lib/src/dashboard/widgets/menu_widget.dart` and add an icon for walletX to be used within the app.
200 +- Go to `lib/src/screens/wallet_list/wallet_list_page.dart` and add an icon for walletx, add a case for walletx also in the `imageFor` method.
201 +- Do the same thing in `lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart`
202 +
203 +- One last thing before we can create a wallet for walletx, go to `lib/view_model/wallet_new_vm.dart`
204 +- Modify the `seedPhraseWordsLength` getter by adding a case for `WalletType.walletx`
205 +
206 +Now you can run the codebase and successfully create a wallet for type walletX successfully.
207 +
208 +**Display Seeds/Keys**
209 +- Next, we want to set up our wallet to display the seeds and/or keys in the security page of the app.
210 +- Go to `lib/view_model/wallet_keys_view_model.dart`
211 +- Modify the `populateItems` function by adding a case for `WalletType.walletx` in it.
212 +- Now your seeds and/or keys should display when you go to Security and Backup -> Show seed/keys page within the app.
213 +
214 +**Restore Wallet**
215 +- Go to `lib/core/seed_validator.dart`
216 +- In the `getWordList` method, add a case to handle `WalletType.walletx` which would return the word list to be used to validate the passed in seeds.
217 +- Next, go to `lib/restore_view_model.dart`
218 +- Modify the `hasRestoreFromPrivateKey` to reflect if walletx supports restore from Key
219 +- Add a switch case to handle the various restore modes that walletX supports
220 +- Modify the `getCredential` method to handle the restore flows for `WalletType.walletx`
221 +- Run the build_runner code generation command
222 +
223 +**Receive**
224 +- Go to `lib/view_model/wallet_address_list/wallet_address_list_view_model.dart`
225 +- Create an implementation of `PaymentUri` for type WalletX.
226 +- In the uri getter, add a case for `WalletType.walletx` returning the implementation class for `PaymentUri`
227 +- Modify the `addressList` getter to return the address/addresses for walletx
228 +
229 +**Balance Screen**
230 +- Go to `lib/view_model/dashboard/balance_view_model.dart`
231 +- Modify the function to adjust the way the balance is being display on the app: `isHomeScreenSettingsEnabled`
232 +- Add a case to the `availableBalanceLabel` getter to modify the text being displayed (Available or confirmed)
233 +- Same for `additionalBalanceLabel`
234 +- Next, go to `lib/reactions/fiat_rate_update.dart`
235 +- Modify the `startFiatRateUpdate` function and add a check for `WalletType.walletx` to return all the token currencies
236 +- Next, go to `lib/reactions/on_current_wallet_change.dart`
237 +- Modify the `startCurrentWalletChangeReaction` function and add a check for `WalletType.walletx` to return all the token currencies
238 +- Lastly, go to `lib/view_model/dashboard/transaction_list_item.dart`
239 +- In the `formattedFiatAmount` getter, add a case to handle the fiat amount conversion for `WalletType.walletx`
240 +
241 +**Send ViewModel**
242 +- Go to `lib/view_model/send/send_view_model.dart`
243 +- Modify the `_credentials` function to reflect `WalletType.walletx`
244 +- Modify `hasMultipleTokens` to reflect wallets
245 +
246 +**Exchange**
247 +- Go to lib/view_model/exchange/exchange_view_model.dart
248 +- First, add a case for WalletType.walletx in the `initialPairBasedOnWallet` method.
249 +- If WalletX supports tokens, go to `lib/view_model/exchange/exchange_trade_view_model.dart`
250 +- Modify the `_checkIfCanSend` method by creating a `_isWalletXToken` that checks if the from currency is WalletX and if its tag is for walletx
251 +- Add `_isWalletXToken` to the return logic for the method.
252 +
253 +**Secrets**
254 +- Create a json file named `wallet-secrets-config.json` and put an empty curly bracket “{}” in it
255 +- Add a new entry to `tool/utils/secret_key.dart` for walletx
256 +- Modify the `tool/generate_secrets_config.dart` file for walletx, don’t forget to call `secrets.clear()` before adding a new set of generation logic
257 +- Modify the `tool/import_secrets_config.dart` file for walletx
258 +- In the `.gitignore` file, add `**/tool/.walletx-secrets-config.json` and `**/cw_walletx/lib/.secrets.g.dart`
259 +
260 +**HomeSettings: WalletX Tokens Display and Management**
261 +- Go to `lib/view_model/dashboard/home_settings_view_model.dart`
262 +- Modify the `_updateTokensList` method to add all walletx tokens if the wallet type is `WalletType.walletx`.
263 +- Modify the `getTokenAddressBasedOnWallet` method to include a case to fetch the address for a WalletX token.
264 +- Modify the `getToken` method to return a specific walletx token
265 +- Modify the `addToken`, `deleteToken` and `changeTokenAvailability` methods to handle cases where the walletType is walletx
266 +
267 +**Buy and Sell WalletX**
268 +- Go to `lib/entities/provider_types.dart`
269 +- Add a case for `WalletType.walletx` in the `getAvailableBuyProviderTypes` method. Return a list of providers that support buying WalletX.
270 +- Add a case for `WalletType.walletx` in the `getAvailableSellProviderTypes` method. Return a list of providers that support selling WalletX.
271 +
272 +**Restore QR setup**
273 +- Go to `lib/view_model/restore/wallet_restore_from_qr_code.dart`
274 +- Add the scheme for walletx in `_walletTypeMap`
275 +- Also modify `_determineWalletRestoreMode` to include a case for walletx
276 +- Go to `lib/view_model/restore/restore_from_qr_vm.dart`
277 +- Modify `getCredentialsFromRestoredWallet` method
278 +- Go to `lib/core/address_validator.dart`
279 +- Modify the `getAddressFromStringPattern` method to add a case for `WalletType.walletx`
280 +- Add the scheme for walletx for both Android in `AndroidManifestBase.xml` and iOS in `InfoBase.plist`
281 +
282 +**Transaction History**
283 +- Go to `lib/view_model/transaction_details_view_model.dart`
284 +- Add a case for `WalletType.walletx` to add the items to be displayed on the detailed view
285 +- Modify the `_explorerUrl` method to add the blockchain explorer link for WalletX in order to view the more info on a transaction
286 +- Modify the `_explorerDescription` to display the name of the explorer
287 +
288 +
289 +
290 +
291 +# Points to note when adding the new wallet type
292 +
293 +1. if it has tokens (ex. ERC20, SPL, etc...) make sure to add that to this function `_checkIfCanSend` in `exchange_trade_view_model.dart`
294 +2. Check On/Off ramp providers that support the new wallet currency and add them accordingly in `provider_types.dart`
295 +3. Add support for wallet uri scheme to restore from QR for both Android in `AndroidManifestBase.xml` and iOS in `InfoBase.plist`
296 +4. Make sure no imports are using the wallet internal package files directly, instead use the proxy layers that is created in the main lib `lib/cw_ethereum.dart` for example. (i.e try building Monero.com if you get compilation errors, then you probably missed something)
297 +5.
298 +
299 +
300 +Copyright (C) 2018-2023 Cake Labs LLC
ios/Runner/InfoBase.plist
+30
@@ -190,6 +190,36 @@
190 <string>solana-wallet</string>
191 </array>
192 </dict>
193 + <dict>
194 + <key>CFBundleTypeRole</key>
195 + <string>Viewer</string>
196 + <key>CFBundleURLName</key>
197 + <string>tron</string>
198 + <key>CFBundleURLSchemes</key>
199 + <array>
200 + <string>tron</string>
201 + </array>
202 + </dict>
203 + <dict>
204 + <key>CFBundleTypeRole</key>
205 + <string>Viewer</string>
206 + <key>CFBundleURLName</key>
207 + <string>tron-wallet</string>
208 + <key>CFBundleURLSchemes</key>
209 + <array>
210 + <string>tron-wallet</string>
211 + </array>
212 + </dict>
213 + <dict>
214 + <key>CFBundleTypeRole</key>
215 + <string>Viewer</string>
216 + <key>CFBundleURLName</key>
217 + <string>tron_wallet</string>
218 + <key>CFBundleURLSchemes</key>
219 + <array>
220 + <string>tron_wallet</string>
221 + </array>
222 + </dict>
223 </array>
224 <key>CFBundleVersion</key>
225 <string>$(CURRENT_PROJECT_VERSION)</string>
lib/core/address_validator.dart
+2
@@ -294,6 +294,8 @@ class AddressValidator extends TextValidator {
294 '|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)';
295 case CryptoCurrency.sol:
296 return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)';
297 + case CryptoCurrency.trx:
298 + return '^(T|t)[1-9A-HJ-NP-Za-km-z]{33}\$';
299 default:
300 if (type.tag == CryptoCurrency.eth.title) {
301 return '0x[0-9a-zA-Z]{42}';
lib/core/seed_validator.dart
+3
@@ -5,6 +5,7 @@ import 'package:cake_wallet/core/validator.dart';
5 import 'package:cake_wallet/entities/mnemonic_item.dart';
6 import 'package:cake_wallet/polygon/polygon.dart';
7 import 'package:cake_wallet/solana/solana.dart';
8 +import 'package:cake_wallet/tron/tron.dart';
9 import 'package:cw_core/wallet_type.dart';
10 import 'package:cake_wallet/monero/monero.dart';
11 import 'package:cake_wallet/nano/nano.dart';
@@ -40,6 +41,8 @@ class SeedValidator extends Validator<MnemonicItem> {
41 return polygon!.getPolygonWordList(language);
42 case WalletType.solana:
43 return solana!.getSolanaWordList(language);
44 + case WalletType.tron:
45 + return tron!.getTronWordList(language);
46 default:
47 return [];
48 }
lib/di.dart
+3
@@ -13,6 +13,7 @@ import 'package:cake_wallet/core/yat_service.dart';
13 import 'package:cake_wallet/entities/background_tasks.dart';
14 import 'package:cake_wallet/entities/exchange_api_mode.dart';
15 import 'package:cake_wallet/entities/parse_address_from_domain.dart';
16 +import 'package:cake_wallet/tron/tron.dart';
17 import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
18 import 'package:cw_core/receive_page_option.dart';
19 import 'package:cake_wallet/ethereum/ethereum.dart';
@@ -873,6 +874,8 @@ Future<void> setup({
874 return polygon!.createPolygonWalletService(_walletInfoSource);
875 case WalletType.solana:
876 return solana!.createSolanaWalletService(_walletInfoSource);
877 + case WalletType.tron:
878 + return tron!.createTronWalletService(_walletInfoSource);
879 default:
880 throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService');
881 }
lib/entities/default_settings_migration.dart
+36 -5
@@ -36,6 +36,7 @@ const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002';
36 const nanoDefaultNodeUri = 'rpc.nano.to';
37 const nanoDefaultPowNodeUri = 'rpc.nano.to';
38 const solanaDefaultNodeUri = 'rpc.ankr.com';
39 +const tronDefaultNodeUri = 'api.trongrid.io';
40 const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002';
41
42 Future<void> defaultSettingsMigration(
@@ -207,23 +208,22 @@ Future<void> defaultSettingsMigration(
208 case 28:
209 await _updateMoneroPriority(sharedPreferences);
210 break;
210 -
211 case 29:
212 await changeDefaultBitcoinNode(nodes, sharedPreferences);
213 break;
214 -
214 case 30:
215 await disableServiceStatusFiatDisabled(sharedPreferences);
216 break;
218 -
217 case 31:
218 await updateNanoNodeList(nodes: nodes);
219 break;
222 -
220 case 32:
221 await updateBtcNanoWalletInfos(walletInfoSource);
222 break;
226 -
223 + case 33:
224 + await addTronNodeList(nodes: nodes);
225 + await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes);
226 + break;
227 default:
228 break;
229 }
@@ -478,6 +478,11 @@ Node? getSolanaDefaultNode({required Box<Node> nodes}) {
478 nodes.values.firstWhereOrNull((node) => node.type == WalletType.solana);
479 }
480
481 +Node? getTronDefaultNode({required Box<Node> nodes}) {
482 + return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == tronDefaultNodeUri) ??
483 + nodes.values.firstWhereOrNull((node) => node.type == WalletType.tron);
484 +}
485 +
486 Future<void> insecureStorageMigration({
487 required SharedPreferences sharedPreferences,
488 required FlutterSecureStorage secureStorage,
@@ -809,6 +814,7 @@ Future<void> checkCurrentNodes(
814 final currentBitcoinCashNodeId =
815 sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey);
816 final currentSolanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey);
817 + final currentTronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey);
818 final currentMoneroNode =
819 nodeSource.values.firstWhereOrNull((node) => node.key == currentMoneroNodeId);
820 final currentBitcoinElectrumServer =
@@ -829,6 +835,8 @@ Future<void> checkCurrentNodes(
835 nodeSource.values.firstWhereOrNull((node) => node.key == currentBitcoinCashNodeId);
836 final currentSolanaNodeServer =
837 nodeSource.values.firstWhereOrNull((node) => node.key == currentSolanaNodeId);
838 + final currentTronNodeServer =
839 + nodeSource.values.firstWhereOrNull((node) => node.key == currentTronNodeId);
840 if (currentMoneroNode == null) {
841 final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero);
842 await nodeSource.add(newCakeWalletNode);
@@ -894,6 +902,12 @@ Future<void> checkCurrentNodes(
902 await nodeSource.add(node);
903 await sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, node.key as int);
904 }
905 +
906 + if (currentTronNodeServer == null) {
907 + final node = Node(uri: tronDefaultNodeUri, type: WalletType.tron);
908 + await nodeSource.add(node);
909 + await sharedPreferences.setInt(PreferencesKey.currentTronNodeIdKey, node.key as int);
910 + }
911 }
912
913 Future<void> resetBitcoinElectrumServer(
@@ -1022,3 +1036,20 @@ Future<void> changeSolanaCurrentNodeToDefault(
1036
1037 await sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, nodeId);
1038 }
1039 +
1040 +Future<void> addTronNodeList({required Box<Node> nodes}) async {
1041 + final nodeList = await loadDefaultTronNodes();
1042 + for (var node in nodeList) {
1043 + if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) {
1044 + await nodes.add(node);
1045 + }
1046 + }
1047 +}
1048 +
1049 +Future<void> changeTronCurrentNodeToDefault(
1050 + {required SharedPreferences sharedPreferences, required Box<Node> nodes}) async {
1051 + final node = getTronDefaultNode(nodes: nodes);
1052 + final nodeId = node?.key as int? ?? 0;
1053 +
1054 + await sharedPreferences.setInt(PreferencesKey.currentTronNodeIdKey, nodeId);
1055 +}
lib/entities/node_list.dart
+19 -1
@@ -166,6 +166,23 @@ Future<List<Node>> loadDefaultSolanaNodes() async {
166 return nodes;
167 }
168
169 +Future<List<Node>> loadDefaultTronNodes() async {
170 + final nodesRaw = await rootBundle.loadString('assets/tron_node_list.yml');
171 + final loadedNodes = loadYaml(nodesRaw) as YamlList;
172 + final nodes = <Node>[];
173 +
174 + for (final raw in loadedNodes) {
175 + if (raw is Map) {
176 + final node = Node.fromMap(Map<String, Object>.from(raw));
177 +
178 + node.type = WalletType.tron;
179 + nodes.add(node);
180 + }
181 + }
182 +
183 + return nodes;
184 +}
185 +
186 Future<void> resetToDefault(Box<Node> nodeSource) async {
187 final moneroNodes = await loadDefaultNodes();
188 final bitcoinElectrumServerList = await loadBitcoinElectrumServerList();
@@ -176,6 +193,7 @@ Future<void> resetToDefault(Box<Node> nodeSource) async {
193 final nanoNodes = await loadDefaultNanoNodes();
194 final polygonNodes = await loadDefaultPolygonNodes();
195 final solanaNodes = await loadDefaultSolanaNodes();
196 + final tronNodes = await loadDefaultTronNodes();
197
198 final nodes = moneroNodes +
199 bitcoinElectrumServerList +
@@ -185,7 +203,7 @@ Future<void> resetToDefault(Box<Node> nodeSource) async {
203 bitcoinCashElectrumServerList +
204 nanoNodes +
205 polygonNodes +
188 - solanaNodes;
206 + solanaNodes + tronNodes;
207
208 await nodeSource.clear();
209 await nodeSource.addAll(nodes);
lib/entities/preferences_key.dart
+1
@@ -14,6 +14,7 @@ class PreferencesKey {
14 static const currentFiatCurrencyKey = 'current_fiat_currency';
15 static const currentBitcoinCashNodeIdKey = 'current_node_id_bch';
16 static const currentSolanaNodeIdKey = 'current_node_id_sol';
17 + static const currentTronNodeIdKey = 'current_node_id_trx';
18 static const currentTransactionPriorityKeyLegacy = 'current_fee_priority';
19 static const currentBalanceDisplayModeKey = 'current_balance_display_mode';
20 static const shouldSaveRecipientAddressKey = 'save_recipient_address';
lib/entities/priority_for_wallet_type.dart
+2 -1
@@ -23,10 +23,11 @@ List<TransactionPriority> priorityForWalletType(WalletType type) {
23 return bitcoinCash!.getTransactionPriorities();
24 case WalletType.polygon:
25 return polygon!.getTransactionPriorities();
26 - // no such thing for nano/banano/solana:
26 + // no such thing for nano/banano/solana/tron:
27 case WalletType.nano:
28 case WalletType.banano:
29 case WalletType.solana:
30 + case WalletType.tron:
31 return [];
32 default:
33 return [];
lib/entities/provider_types.dart
+13
@@ -69,6 +69,13 @@ class ProvidersHelper {
69 return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay];
70 case WalletType.solana:
71 return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
72 + case WalletType.tron:
73 + return [
74 + ProviderType.askEachTime,
75 + ProviderType.onramper,
76 + ProviderType.robinhood,
77 + ProviderType.moonpay,
78 + ];
79 case WalletType.none:
80 case WalletType.haven:
81 return [];
@@ -96,6 +103,12 @@ class ProvidersHelper {
103 ProviderType.robinhood,
104 ProviderType.moonpay,
105 ];
106 + case WalletType.tron:
107 + return [
108 + ProviderType.askEachTime,
109 + ProviderType.robinhood,
110 + ProviderType.moonpay,
111 + ];
112 case WalletType.monero:
113 case WalletType.nano:
114 case WalletType.banano:
lib/main.dart
+1 -1
@@ -167,7 +167,7 @@ Future<void> initializeAppConfigs() async {
167 transactionDescriptions: transactionDescriptions,
168 secureStorage: secureStorage,
169 anonpayInvoiceInfo: anonpayInvoiceInfo,
170 - initialMigrationVersion: 32,
170 + initialMigrationVersion: 33,
171 );
172 }
173
lib/reactions/fiat_rate_update.dart
+6
@@ -8,6 +8,7 @@ import 'package:cake_wallet/solana/solana.dart';
8 import 'package:cake_wallet/store/app_store.dart';
9 import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
10 import 'package:cake_wallet/store/settings_store.dart';
11 +import 'package:cake_wallet/tron/tron.dart';
12 import 'package:cw_core/crypto_currency.dart';
13 import 'package:cw_core/erc20_token.dart';
14 import 'package:cw_core/wallet_type.dart';
@@ -53,6 +54,11 @@ Future<void> startFiatRateUpdate(
54 solana!.getSPLTokenCurrencies(appStore.wallet!).where((element) => element.enabled);
55 }
56
57 + if (appStore.wallet!.type == WalletType.tron) {
58 + currencies =
59 + tron!.getTronTokenCurrencies(appStore.wallet!).where((element) => element.enabled);
60 + }
61 +
62
63 if (currencies != null) {
64 for (final currency in currencies) {
lib/reactions/on_current_wallet_change.dart
+10 -4
@@ -4,8 +4,8 @@ import 'package:cake_wallet/entities/update_haven_rate.dart';
4 import 'package:cake_wallet/ethereum/ethereum.dart';
5 import 'package:cake_wallet/polygon/polygon.dart';
6 import 'package:cake_wallet/solana/solana.dart';
7 +import 'package:cake_wallet/tron/tron.dart';
8 import 'package:cw_core/crypto_currency.dart';
8 -import 'package:cw_core/erc20_token.dart';
9 import 'package:cw_core/transaction_history.dart';
10 import 'package:cw_core/balance.dart';
11 import 'package:cw_core/transaction_info.dart';
@@ -70,8 +70,10 @@ void startCurrentWalletChangeReaction(
70 .get<SharedPreferences>()
71 .setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type));
72
73 - if (wallet.type == WalletType.monero || wallet.type == WalletType.bitcoin ||
74 - wallet.type == WalletType.litecoin || wallet.type == WalletType.bitcoinCash ) {
73 + if (wallet.type == WalletType.monero ||
74 + wallet.type == WalletType.bitcoin ||
75 + wallet.type == WalletType.litecoin ||
76 + wallet.type == WalletType.bitcoinCash) {
77 _setAutoGenerateSubaddressStatus(wallet, settingsStore);
78 }
79
@@ -124,7 +126,11 @@ void startCurrentWalletChangeReaction(
126 currencies =
127 solana!.getSPLTokenCurrencies(appStore.wallet!).where((element) => element.enabled);
128 }
127 -
129 + if (wallet.type == WalletType.tron) {
130 + currencies =
131 + tron!.getTronTokenCurrencies(appStore.wallet!).where((element) => element.enabled);
132 + }
133 +
134 if (currencies != null) {
135 for (final currency in currencies) {
136 () async {
lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart
+3
@@ -38,6 +38,7 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
38 final nanoIcon = Image.asset('assets/images/nano_icon.png', height: 24, width: 24);
39 final bananoIcon = Image.asset('assets/images/nano_icon.png', height: 24, width: 24);
40 final solanaIcon = Image.asset('assets/images/sol_icon.png', height: 24, width: 24);
41 + final tronIcon = Image.asset('assets/images/trx_icon.png', height: 24, width: 24);
42 final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24);
43
44 Image _newWalletImage(BuildContext context) => Image.asset(
@@ -156,6 +157,8 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
157 return polygonIcon;
158 case WalletType.solana:
159 return solanaIcon;
160 + case WalletType.tron:
161 + return tronIcon;
162 default:
163 return nonWalletTypeIcon;
164 }
lib/src/screens/dashboard/pages/transactions_page.dart
+18 -12
@@ -82,21 +82,27 @@ class TransactionsPage extends StatelessWidget {
82 }
83
84 if (item is TransactionListItem) {
85 + if (item.hasTokens && item.assetOfTransaction == null) {
86 + return Container();
87 + }
88 +
89 final transaction = item.transaction;
90
91 return Observer(
88 - builder: (_) => TransactionRow(
89 - onTap: () => Navigator.of(context)
90 - .pushNamed(Routes.transactionDetails, arguments: transaction),
91 - direction: transaction.direction,
92 - formattedDate: DateFormat('HH:mm').format(transaction.date),
93 - formattedAmount: item.formattedCryptoAmount,
94 - formattedFiatAmount:
95 - dashboardViewModel.balanceViewModel.isFiatDisabled
96 - ? ''
97 - : item.formattedFiatAmount,
98 - isPending: transaction.isPending,
99 - title: item.formattedTitle + item.formattedStatus));
92 + builder: (_) => TransactionRow(
93 + onTap: () => Navigator.of(context)
94 + .pushNamed(Routes.transactionDetails, arguments: transaction),
95 + direction: transaction.direction,
96 + formattedDate: DateFormat('HH:mm').format(transaction.date),
97 + formattedAmount: item.formattedCryptoAmount,
98 + formattedFiatAmount:
99 + dashboardViewModel.balanceViewModel.isFiatDisabled
100 + ? ''
101 + : item.formattedFiatAmount,
102 + isPending: transaction.isPending,
103 + title: item.formattedTitle + item.formattedStatus,
104 + ),
105 + );
106 }
107
108 if (item is AnonpayTransactionListItem) {
lib/src/screens/dashboard/widgets/menu_widget.dart
+5 -1
@@ -34,7 +34,8 @@ class MenuWidgetState extends State<MenuWidget> {
34 this.bananoIcon = Image.asset('assets/images/nano_icon.png'),
35 this.bitcoinCashIcon = Image.asset('assets/images/bch_icon.png'),
36 this.polygonIcon = Image.asset('assets/images/matic_icon.png'),
37 - this.solanaIcon = Image.asset('assets/images/sol_icon.png');
37 + this.solanaIcon = Image.asset('assets/images/sol_icon.png'),
38 + this.tronIcon = Image.asset('assets/images/trx_icon.png');
39
40 final largeScreen = 731;
41
@@ -57,6 +58,7 @@ class MenuWidgetState extends State<MenuWidget> {
58 Image bananoIcon;
59 Image polygonIcon;
60 Image solanaIcon;
61 + Image tronIcon;
62
63 @override
64 void initState() {
@@ -226,6 +228,8 @@ class MenuWidgetState extends State<MenuWidget> {
228 return polygonIcon;
229 case WalletType.solana:
230 return solanaIcon;
231 + case WalletType.tron:
232 + return tronIcon;
233 default:
234 throw Exception('No icon for ${type.toString()}');
235 }
lib/src/screens/wallet_list/wallet_list_page.dart
+3
@@ -104,6 +104,7 @@ class WalletListBodyState extends State<WalletListBody> {
104 final nanoIcon = Image.asset('assets/images/nano_icon.png', height: 24, width: 24);
105 final polygonIcon = Image.asset('assets/images/matic_icon.png', height: 24, width: 24);
106 final solanaIcon = Image.asset('assets/images/sol_icon.png', height: 24, width: 24);
107 + final tronIcon = Image.asset('assets/images/trx_icon.png', height: 24, width: 24);
108 final scrollController = ScrollController();
109 final double tileHeight = 60;
110 Flushbar<void>? _progressBar;
@@ -316,6 +317,8 @@ class WalletListBodyState extends State<WalletListBody> {
317 return polygonIcon;
318 case WalletType.solana:
319 return solanaIcon;
320 + case WalletType.tron:
321 + return tronIcon;
322 default:
323 return nonWalletTypeIcon;
324 }
lib/store/settings_store.dart
+15
@@ -872,6 +872,7 @@ abstract class SettingsStoreBase with Store {
872 final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey);
873 final nanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey);
874 final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey);
875 + final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey);
876 final moneroNode = nodeSource.get(nodeId);
877 final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId);
878 final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId);
@@ -882,6 +883,7 @@ abstract class SettingsStoreBase with Store {
883 final nanoNode = nodeSource.get(nanoNodeId);
884 final nanoPowNode = powNodeSource.get(nanoPowNodeId);
885 final solanaNode = nodeSource.get(solanaNodeId);
886 + final tronNode = nodeSource.get(tronNodeId);
887 final packageInfo = await PackageInfo.fromPlatform();
888 final deviceName = await _getDeviceName() ?? '';
889 final shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true;
@@ -944,6 +946,10 @@ abstract class SettingsStoreBase with Store {
946 nodes[WalletType.solana] = solanaNode;
947 }
948
949 + if (tronNode != null) {
950 + nodes[WalletType.tron] = tronNode;
951 + }
952 +
953 final savedSyncMode = SyncMode.all.firstWhere((element) {
954 return element.type.index == (sharedPreferences.getInt(PreferencesKey.syncModeKey) ?? 0);
955 });
@@ -1238,6 +1244,7 @@ abstract class SettingsStoreBase with Store {
1244 final polygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey);
1245 final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey);
1246 final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey);
1247 + final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey);
1248 final moneroNode = nodeSource.get(nodeId);
1249 final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId);
1250 final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId);
@@ -1247,6 +1254,7 @@ abstract class SettingsStoreBase with Store {
1254 final bitcoinCashNode = nodeSource.get(bitcoinCashElectrumServerId);
1255 final nanoNode = nodeSource.get(nanoNodeId);
1256 final solanaNode = nodeSource.get(solanaNodeId);
1257 + final tronNode = nodeSource.get(tronNodeId);
1258 if (moneroNode != null) {
1259 nodes[WalletType.monero] = moneroNode;
1260 }
@@ -1283,6 +1291,10 @@ abstract class SettingsStoreBase with Store {
1291 nodes[WalletType.solana] = solanaNode;
1292 }
1293
1294 + if (tronNode != null) {
1295 + nodes[WalletType.tron] = tronNode;
1296 + }
1297 +
1298 // MIGRATED:
1299
1300 useTOTP2FA = await SecureKey.getBool(
@@ -1413,6 +1425,9 @@ abstract class SettingsStoreBase with Store {
1425 case WalletType.solana:
1426 await _sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, node.key as int);
1427 break;
1428 + case WalletType.tron:
1429 + await _sharedPreferences.setInt(PreferencesKey.currentTronNodeIdKey, node.key as int);
1430 + break;
1431 default:
1432 break;
1433 }
lib/tron/cw_tron.dart new
+114
@@ -0,0 +1,114 @@
1 +part of 'tron.dart';
2 +
3 +class CWTron extends Tron {
4 + @override
5 + List<String> getTronWordList(String language) => EVMChainMnemonics.englishWordlist;
6 +
7 + WalletService createTronWalletService(Box<WalletInfo> walletInfoSource) =>
8 + TronWalletService(walletInfoSource, client: TronClient());
9 +
10 + @override
11 + WalletCredentials createTronNewWalletCredentials({
12 + required String name,
13 + WalletInfo? walletInfo,
14 + }) =>
15 + TronNewWalletCredentials(name: name, walletInfo: walletInfo);
16 +
17 + @override
18 + WalletCredentials createTronRestoreWalletFromSeedCredentials({
19 + required String name,
20 + required String mnemonic,
21 + required String password,
22 + }) =>
23 + TronRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic);
24 +
25 + @override
26 + WalletCredentials createTronRestoreWalletFromPrivateKey({
27 + required String name,
28 + required String privateKey,
29 + required String password,
30 + }) =>
31 + TronRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey);
32 +
33 + @override
34 + String getAddress(WalletBase wallet) => (wallet as TronWallet).walletAddresses.address;
35 +
36 + Object createTronTransactionCredentials(
37 + List<Output> outputs, {
38 + required CryptoCurrency currency,
39 + }) =>
40 + TronTransactionCredentials(
41 + outputs
42 + .map(
43 + (out) => OutputInfo(
44 + fiatAmount: out.fiatAmount,
45 + cryptoAmount: out.cryptoAmount,
46 + address: out.address,
47 + note: out.note,
48 + sendAll: out.sendAll,
49 + extractedAddress: out.extractedAddress,
50 + isParsedAddress: out.isParsedAddress,
51 + formattedCryptoAmount: out.formattedCryptoAmount,
52 + ),
53 + )
54 + .toList(),
55 + currency: currency,
56 + );
57 +
58 + @override
59 + List<TronToken> getTronTokenCurrencies(WalletBase wallet) =>
60 + (wallet as TronWallet).tronTokenCurrencies;
61 +
62 + @override
63 + Future<void> addTronToken(WalletBase wallet, CryptoCurrency token, String contractAddress) async {
64 + final tronToken = TronToken(
65 + name: token.name,
66 + symbol: token.title,
67 + contractAddress: contractAddress,
68 + decimal: token.decimals,
69 + enabled: token.enabled,
70 + iconPath: token.iconPath,
71 + );
72 + await (wallet as TronWallet).addTronToken(tronToken);
73 + }
74 +
75 + @override
76 + Future<void> deleteTronToken(WalletBase wallet, CryptoCurrency token) async =>
77 + await (wallet as TronWallet).deleteTronToken(token as TronToken);
78 +
79 + @override
80 + Future<TronToken?> getTronToken(WalletBase wallet, String contractAddress) async =>
81 + (wallet as TronWallet).getTronToken(contractAddress);
82 +
83 + @override
84 + double getTransactionAmountRaw(TransactionInfo transactionInfo) {
85 + final amount = (transactionInfo as TronTransactionInfo).rawTronAmount();
86 + return double.parse(amount);
87 + }
88 +
89 + @override
90 + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) {
91 + transaction as TronTransactionInfo;
92 + if (transaction.tokenSymbol == CryptoCurrency.trx.title) {
93 + return CryptoCurrency.trx;
94 + }
95 +
96 + wallet as TronWallet;
97 + return wallet.tronTokenCurrencies.firstWhere(
98 + (element) => transaction.tokenSymbol.toLowerCase() == element.symbol.toLowerCase());
99 + }
100 +
101 + @override
102 + String getTokenAddress(CryptoCurrency asset) => (asset as TronToken).contractAddress;
103 +
104 + @override
105 + String getTronBase58Address(String hexAddress, WalletBase wallet) =>
106 + (wallet as TronWallet).getTronBase58AddressFromHex(hexAddress);
107 +
108 + @override
109 + String? getTronNativeEstimatedFee(WalletBase wallet) =>
110 + (wallet as TronWallet).nativeTxEstimatedFee;
111 +
112 + @override
113 + String? getTronTRC20EstimatedFee(WalletBase wallet) => (wallet as TronWallet).trc20EstimatedFee;
114 +}
lib/view_model/advanced_privacy_settings_view_model.dart
+1
@@ -38,6 +38,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store {
38 case WalletType.bitcoinCash:
39 case WalletType.polygon:
40 case WalletType.solana:
41 + case WalletType.tron:
42 return true;
43 case WalletType.monero:
44 case WalletType.none:
lib/view_model/dashboard/balance_view_model.dart
+6 -1
@@ -80,7 +80,9 @@ abstract class BalanceViewModelBase with Store {
80
81 @computed
82 bool get isHomeScreenSettingsEnabled =>
83 - isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana;
83 + isEVMCompatibleChain(wallet.type) ||
84 + wallet.type == WalletType.solana ||
85 + wallet.type == WalletType.tron;
86
87 @computed
88 bool get hasAccounts => wallet.type == WalletType.monero;
@@ -126,6 +128,7 @@ abstract class BalanceViewModelBase with Store {
128 case WalletType.nano:
129 case WalletType.banano:
130 case WalletType.solana:
131 + case WalletType.tron:
132 return S.current.xmr_available_balance;
133 default:
134 return S.current.confirmed;
@@ -140,6 +143,7 @@ abstract class BalanceViewModelBase with Store {
143 case WalletType.ethereum:
144 case WalletType.polygon:
145 case WalletType.solana:
146 + case WalletType.tron:
147 return S.current.xmr_full_balance;
148 case WalletType.nano:
149 case WalletType.banano:
@@ -287,6 +291,7 @@ abstract class BalanceViewModelBase with Store {
291 case WalletType.ethereum:
292 case WalletType.polygon:
293 case WalletType.solana:
294 + case WalletType.tron:
295 return false;
296 default:
297 return true;
lib/view_model/dashboard/home_settings_view_model.dart
+31 -2
@@ -5,6 +5,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart';
5 import 'package:cake_wallet/polygon/polygon.dart';
6 import 'package:cake_wallet/solana/solana.dart';
7 import 'package:cake_wallet/store/settings_store.dart';
8 +import 'package:cake_wallet/tron/tron.dart';
9 import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
10 import 'package:cw_core/crypto_currency.dart';
11 import 'package:cw_core/erc20_token.dart';
@@ -79,6 +80,10 @@ abstract class HomeSettingsViewModelBase with Store {
80 );
81 }
82
83 + if (_balanceViewModel.wallet.type == WalletType.tron) {
84 + await tron!.addTronToken(_balanceViewModel.wallet, token, contractAddress);
85 + }
86 +
87 _updateTokensList();
88 _updateFiatPrices(token);
89 }
@@ -96,6 +101,9 @@ abstract class HomeSettingsViewModelBase with Store {
101 await solana!.deleteSPLToken(_balanceViewModel.wallet, token);
102 }
103
104 + if (_balanceViewModel.wallet.type == WalletType.tron) {
105 + await tron!.deleteTronToken(_balanceViewModel.wallet, token);
106 + }
107 _updateTokensList();
108 }
109
@@ -112,6 +120,10 @@ abstract class HomeSettingsViewModelBase with Store {
120 return await solana!.getSPLToken(_balanceViewModel.wallet, contractAddress);
121 }
122
123 + if (_balanceViewModel.wallet.type == WalletType.tron) {
124 + return await tron!.getTronToken(_balanceViewModel.wallet, contractAddress);
125 + }
126 +
127 return null;
128 }
129
@@ -143,6 +155,11 @@ abstract class HomeSettingsViewModelBase with Store {
155 solana!.addSPLToken(_balanceViewModel.wallet, token, address);
156 }
157
158 + if (_balanceViewModel.wallet.type == WalletType.tron) {
159 + final address = tron!.getTokenAddress(token);
160 + tron!.addTronToken(_balanceViewModel.wallet, token, address);
161 + }
162 +
163 _refreshTokensList();
164 }
165
@@ -189,6 +206,14 @@ abstract class HomeSettingsViewModelBase with Store {
206 .toList()
207 ..sort(_sortFunc));
208 }
209 +
210 + if (_balanceViewModel.wallet.type == WalletType.tron) {
211 + tokens.addAll(tron!
212 + .getTronTokenCurrencies(_balanceViewModel.wallet)
213 + .where((element) => _matchesSearchText(element))
214 + .toList()
215 + ..sort(_sortFunc));
216 + }
217 }
218
219 @action
@@ -207,7 +232,7 @@ abstract class HomeSettingsViewModelBase with Store {
232 bool _matchesSearchText(CryptoCurrency asset) {
233 final address = getTokenAddressBasedOnWallet(asset);
234
210 - // The homes settings would only be displayed for either of Ethereum, Polygon or Solana Wallets.
235 + // The homes settings would only be displayed for either of Tron, Ethereum, Polygon or Solana Wallets.
236 if (address == null) return false;
237
238 return searchText.isEmpty ||
@@ -217,6 +242,10 @@ abstract class HomeSettingsViewModelBase with Store {
242 }
243
244 String? getTokenAddressBasedOnWallet(CryptoCurrency asset) {
245 + if (_balanceViewModel.wallet.type == WalletType.tron) {
246 + return tron!.getTokenAddress(asset);
247 + }
248 +
249 if (_balanceViewModel.wallet.type == WalletType.solana) {
250 return solana!.getTokenAddress(asset);
251 }
@@ -229,7 +258,7 @@ abstract class HomeSettingsViewModelBase with Store {
258 return polygon!.getTokenAddress(asset);
259 }
260
232 - // We return null if it's neither Polygin, Ethereum or Solana wallet (which is actually impossible because we only display home settings for either of these three wallets).
261 + // We return null if it's neither Tron, Polygon, Ethereum or Solana wallet (which is actually impossible because we only display home settings for either of these three wallets).
262 return null;
263 }
264 }
lib/view_model/dashboard/transaction_list_item.dart
+46
@@ -4,7 +4,10 @@ import 'package:cake_wallet/ethereum/ethereum.dart';
4 import 'package:cake_wallet/generated/i18n.dart';
5 import 'package:cake_wallet/nano/nano.dart';
6 import 'package:cake_wallet/polygon/polygon.dart';
7 +import 'package:cake_wallet/reactions/wallet_connect.dart';
8 import 'package:cake_wallet/solana/solana.dart';
9 +import 'package:cake_wallet/tron/tron.dart';
10 +import 'package:cw_core/crypto_currency.dart';
11 import 'package:cw_core/transaction_direction.dart';
12 import 'package:cw_core/transaction_info.dart';
13 import 'package:cake_wallet/store/settings_store.dart';
@@ -34,6 +37,11 @@ class TransactionListItem extends ActionListItem with Keyable {
37 @override
38 dynamic get keyIndex => transaction.id;
39
40 + bool get hasTokens =>
41 + isEVMCompatibleChain(balanceViewModel.wallet.type) ||
42 + balanceViewModel.wallet.type == WalletType.solana ||
43 + balanceViewModel.wallet.type == WalletType.tron;
44 +
45 String get formattedCryptoAmount {
46 return displayMode == BalanceDisplayMode.hiddenBalance ? '---' : transaction.amountFormatted();
47 }
@@ -63,6 +71,34 @@ class TransactionListItem extends ActionListItem with Keyable {
71 return transaction.isPending ? S.current.pending : '';
72 }
73
74 + CryptoCurrency? get assetOfTransaction {
75 + try {
76 + if (balanceViewModel.wallet.type == WalletType.ethereum) {
77 + final asset = ethereum!.assetOfTransaction(balanceViewModel.wallet, transaction);
78 + return asset;
79 + }
80 +
81 + if (balanceViewModel.wallet.type == WalletType.polygon) {
82 + final asset = polygon!.assetOfTransaction(balanceViewModel.wallet, transaction);
83 + return asset;
84 + }
85 +
86 + if (balanceViewModel.wallet.type == WalletType.solana) {
87 + final asset = solana!.assetOfTransaction(balanceViewModel.wallet, transaction);
88 + return asset;
89 + }
90 +
91 + if (balanceViewModel.wallet.type == WalletType.tron) {
92 + final asset = tron!.assetOfTransaction(balanceViewModel.wallet, transaction);
93 + return asset;
94 + }
95 + } catch (e) {
96 + return null;
97 + }
98 +
99 + return null;
100 + }
101 +
102 String get formattedFiatAmount {
103 var amount = '';
104
@@ -114,6 +150,16 @@ class TransactionListItem extends ActionListItem with Keyable {
150 price: price,
151 );
152 break;
153 +
154 + case WalletType.tron:
155 + final asset = tron!.assetOfTransaction(balanceViewModel.wallet, transaction);
156 + final price = balanceViewModel.fiatConvertationStore.prices[asset];
157 + final cryptoAmount = tron!.getTransactionAmountRaw(transaction);
158 + amount = calculateFiatAmountRaw(
159 + cryptoAmount: cryptoAmount,
160 + price: price,
161 + );
162 + break;
163 default:
164 break;
165 }
lib/view_model/exchange/exchange_trade_view_model.dart
+6 -1
@@ -178,6 +178,10 @@ abstract class ExchangeTradeViewModelBase with Store {
178 wallet.currency == CryptoCurrency.maticpoly &&
179 tradesStore.trade!.from.tag == CryptoCurrency.maticpoly.tag;
180
181 + bool _isTronToken() =>
182 + wallet.currency == CryptoCurrency.trx &&
183 + tradesStore.trade!.from.tag == CryptoCurrency.trx.title;
184 +
185 bool _isSplToken() =>
186 wallet.currency == CryptoCurrency.sol &&
187 tradesStore.trade!.from.tag == CryptoCurrency.sol.title;
@@ -186,6 +190,7 @@ abstract class ExchangeTradeViewModelBase with Store {
190 tradesStore.trade!.provider == ExchangeProviderDescription.xmrto ||
191 _isEthToken() ||
192 _isPolygonToken() ||
189 - _isSplToken();
193 + _isSplToken() ||
194 + _isTronToken();
195 }
196 }
lib/view_model/exchange/exchange_view_model.dart
+4
@@ -676,6 +676,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
676 depositCurrency = CryptoCurrency.sol;
677 receiveCurrency = CryptoCurrency.xmr;
678 break;
679 + case WalletType.tron:
680 + depositCurrency = CryptoCurrency.trx;
681 + receiveCurrency = CryptoCurrency.xmr;
682 + break;
683 default:
684 break;
685 }
lib/view_model/node_list/node_create_or_edit_view_model.dart
+1
@@ -76,6 +76,7 @@ abstract class NodeCreateOrEditViewModelBase with Store {
76 case WalletType.solana:
77 case WalletType.banano:
78 case WalletType.nano:
79 + case WalletType.tron:
80 return true;
81 case WalletType.none:
82 case WalletType.monero:
lib/view_model/node_list/node_list_view_model.dart
+3
@@ -82,6 +82,9 @@ abstract class NodeListViewModelBase with Store {
82 case WalletType.solana:
83 node = getSolanaDefaultNode(nodes: _nodeSource)!;
84 break;
85 + case WalletType.tron:
86 + node = getTronDefaultNode(nodes: _nodeSource)!;
87 + break;
88 default:
89 throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}');
90 }
lib/view_model/restore/restore_from_qr_vm.dart
+8 -1
@@ -4,13 +4,14 @@ import 'package:cake_wallet/ethereum/ethereum.dart';
4 import 'package:cake_wallet/nano/nano.dart';
5 import 'package:cake_wallet/polygon/polygon.dart';
6 import 'package:cake_wallet/solana/solana.dart';
7 +import 'package:cake_wallet/tron/tron.dart';
8 import 'package:cake_wallet/view_model/restore/restore_mode.dart';
9 import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
10 import 'package:hive/hive.dart';
11 import 'package:mobx/mobx.dart';
12 import 'package:cake_wallet/monero/monero.dart';
13 import 'package:cake_wallet/store/app_store.dart';
13 -import 'package:cw_core/wallet_base.dart';
14 +import 'package:cw_core/wallet_base.dart';
15 import 'package:cake_wallet/core/generate_wallet_password.dart';
16 import 'package:cake_wallet/core/wallet_creation_service.dart';
17 import 'package:cw_core/wallet_credentials.dart';
@@ -86,6 +87,9 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store
87 case WalletType.solana:
88 return solana!.createSolanaRestoreWalletFromPrivateKey(
89 name: name, password: password, privateKey: restoreWallet.privateKey!);
90 + case WalletType.tron:
91 + return tron!.createTronRestoreWalletFromPrivateKey(
92 + name: name, password: password, privateKey: restoreWallet.privateKey!);
93 default:
94 throw Exception('Unexpected type: ${restoreWallet.type.toString()}');
95 }
@@ -130,6 +134,9 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store
134 case WalletType.solana:
135 return solana!.createSolanaRestoreWalletFromSeedCredentials(
136 name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password);
137 + case WalletType.tron:
138 + return tron!.createTronRestoreWalletFromSeedCredentials(
139 + name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password);
140 default:
141 throw Exception('Unexpected type: ${type.toString()}');
142 }
lib/view_model/restore/wallet_restore_from_qr_code.dart
+11
@@ -33,6 +33,9 @@ class WalletRestoreFromQRCode {
33 'bitcoincash-wallet': WalletType.bitcoinCash,
34 'bitcoincash_wallet': WalletType.bitcoinCash,
35 'solana-wallet': WalletType.solana,
36 + 'tron': WalletType.tron,
37 + 'tron-wallet': WalletType.tron,
38 + 'tron_wallet': WalletType.tron,
39 };
40
41 static bool _containsAssetSpecifier(String code) => _extractWalletType(code) != null;
@@ -184,6 +187,14 @@ class WalletRestoreFromQRCode {
187 return WalletRestoreMode.keys;
188 }
189
190 + if (type == WalletType.tron && credentials.containsKey('private_key')) {
191 + final privateKey = credentials['private_key'] as String;
192 + if (privateKey.isEmpty) {
193 + throw Exception('Unexpected restore mode: private_key');
194 + }
195 + return WalletRestoreMode.keys;
196 + }
197 +
198 throw Exception('Unexpected restore mode: restore params are invalid');
199 }
200 }
lib/view_model/send/output.dart
+20 -2
@@ -8,6 +8,7 @@ import 'package:cake_wallet/polygon/polygon.dart';
8 import 'package:cake_wallet/reactions/wallet_connect.dart';
9 import 'package:cake_wallet/solana/solana.dart';
10 import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
11 +import 'package:cake_wallet/tron/tron.dart';
12 import 'package:cw_core/crypto_currency.dart';
13 import 'package:flutter/material.dart';
14 import 'package:intl/intl.dart';
@@ -117,6 +118,17 @@ abstract class OutputBase with Store {
118 @computed
119 double get estimatedFee {
120 try {
121 + if (_wallet.type == WalletType.tron) {
122 + if (cryptoCurrencyHandler() == CryptoCurrency.trx) {
123 + final nativeEstimatedFee = tron!.getTronNativeEstimatedFee(_wallet) ?? 0;
124 + return double.parse(nativeEstimatedFee.toString());
125 + } else {
126 + final trc20EstimatedFee = tron!.getTronTRC20EstimatedFee(_wallet) ?? 0;
127 + return double.parse(trc20EstimatedFee.toString());
128 + }
129 +
130 + }
131 +
132 if (_wallet.type == WalletType.solana) {
133 return solana!.getEstimateFees(_wallet) ?? 0.0;
134 }
@@ -163,8 +175,11 @@ abstract class OutputBase with Store {
175 @computed
176 String get estimatedFeeFiatAmount {
177 try {
166 - final currency =
167 - isEVMCompatibleChain(_wallet.type) ? _wallet.currency : cryptoCurrencyHandler();
178 + final currency = (isEVMCompatibleChain(_wallet.type) ||
179 + _wallet.type == WalletType.solana ||
180 + _wallet.type == WalletType.tron)
181 + ? _wallet.currency
182 + : cryptoCurrencyHandler();
183 final fiat = calculateFiatAmountRaw(
184 price: _fiatConversationStore.prices[currency]!, cryptoAmount: estimatedFee);
185 return fiat;
@@ -269,6 +284,9 @@ abstract class OutputBase with Store {
284 case WalletType.solana:
285 maximumFractionDigits = 12;
286 break;
287 + case WalletType.tron:
288 + maximumFractionDigits = 12;
289 + break;
290 default:
291 break;
292 }
lib/view_model/send/send_template_view_model.dart
+2 -1
@@ -53,7 +53,8 @@ abstract class SendTemplateViewModelBase with Store {
53 _wallet.type != WalletType.haven &&
54 _wallet.type != WalletType.ethereum &&
55 _wallet.type != WalletType.polygon &&
56 - _wallet.type != WalletType.solana;
56 + _wallet.type != WalletType.solana &&
57 + _wallet.type != WalletType.tron;
58
59 @computed
60 CryptoCurrency get cryptoCurrency => _wallet.currency;
lib/view_model/send/send_view_model.dart
+15 -4
@@ -12,6 +12,7 @@ import 'package:cake_wallet/polygon/polygon.dart';
12 import 'package:cake_wallet/reactions/wallet_connect.dart';
13 import 'package:cake_wallet/solana/solana.dart';
14 import 'package:cake_wallet/store/app_store.dart';
15 +import 'package:cake_wallet/tron/tron.dart';
16 import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
17 import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
18 import 'package:cw_core/exceptions.dart';
@@ -50,7 +51,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
51 void onWalletChange(wallet) {
52 currencies = wallet.balance.keys.toList();
53 selectedCryptoCurrency = wallet.currency;
53 - hasMultipleTokens = isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana;
54 + hasMultipleTokens = isEVMCompatibleChain(wallet.type) ||
55 + wallet.type == WalletType.solana ||
56 + wallet.type == WalletType.tron;
57 }
58
59 SendViewModelBase(
@@ -64,7 +67,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
67 currencies = appStore.wallet!.balance.keys.toList(),
68 selectedCryptoCurrency = appStore.wallet!.currency,
69 hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type) ||
67 - appStore.wallet!.type == WalletType.solana,
70 + appStore.wallet!.type == WalletType.solana ||
71 + appStore.wallet!.type == WalletType.tron,
72 outputs = ObservableList<Output>(),
73 _settingsStore = appStore.settingsStore,
74 fiatFromSettings = appStore.settingsStore.fiatCurrency,
@@ -110,6 +114,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
114 @computed
115 bool get isBatchSending => outputs.length > 1;
116
117 + bool get shouldDisplaySendALL => walletType != WalletType.solana || walletType != WalletType.tron;
118 +
119 @computed
120 String get pendingTransactionFiatAmount {
121 if (pendingTransaction == null) {
@@ -236,7 +242,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
242 bool get hasFeesPriority =>
243 wallet.type != WalletType.nano &&
244 wallet.type != WalletType.banano &&
239 - wallet.type != WalletType.solana;
245 + wallet.type != WalletType.solana &&
246 + wallet.type != WalletType.tron;
247 +
248 @observable
249 CryptoCurrency selectedCryptoCurrency;
250
@@ -423,7 +431,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
431 Object _credentials() {
432 final priority = _settingsStore.priority[wallet.type];
433
426 - if (priority == null && wallet.type != WalletType.nano && wallet.type != WalletType.banano && wallet.type != WalletType.solana) {
434 + if (priority == null && wallet.type != WalletType.nano && wallet.type != WalletType.banano && wallet.type != WalletType.solana &&
435 + wallet.type != WalletType.tron) {
436 throw Exception('Priority is null for wallet type: ${wallet.type}');
437 }
438
@@ -453,6 +462,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
462 case WalletType.solana:
463 return solana!
464 .createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency);
465 + case WalletType.tron:
466 + return tron!.createTronTransactionCredentials(outputs, currency: selectedCryptoCurrency);
467 default:
468 throw Exception('Unexpected wallet type: ${wallet.type}');
469 }
lib/view_model/settings/other_settings_view_model.dart
+3 -2
@@ -56,8 +56,9 @@ abstract class OtherSettingsViewModelBase with Store {
56 _wallet.type == WalletType.nano || _wallet.type == WalletType.banano;
57
58 @computed
59 - bool get displayTransactionPriority =>
60 - !(changeRepresentativeEnabled || _wallet.type == WalletType.solana);
59 + bool get displayTransactionPriority => !(changeRepresentativeEnabled ||
60 + _wallet.type == WalletType.solana ||
61 + _wallet.type == WalletType.tron);
62
63 @computed
64 bool get isEnabledBuyAction => !_settingsStore.disableBuy && _wallet.type != WalletType.haven;
lib/view_model/transaction_details_view_model.dart
+41 -13
@@ -1,3 +1,7 @@
1 +import 'package:cake_wallet/tron/tron.dart';
2 +import 'package:cw_core/wallet_base.dart';
3 +import 'package:cw_core/transaction_info.dart';
4 +import 'package:cw_core/wallet_type.dart';
5 import 'package:cake_wallet/bitcoin/bitcoin.dart';
6 import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
7 import 'package:cake_wallet/entities/transaction_description.dart';
@@ -14,10 +18,7 @@ import 'package:cake_wallet/utils/date_formatter.dart';
18 import 'package:cake_wallet/view_model/send/send_view_model.dart';
19 import 'package:collection/collection.dart';
20 import 'package:cw_core/transaction_direction.dart';
17 -import 'package:cw_core/transaction_info.dart';
21 import 'package:cw_core/transaction_priority.dart';
19 -import 'package:cw_core/wallet_base.dart';
20 -import 'package:cw_core/wallet_type.dart';
22 import 'package:hive/hive.dart';
23 import 'package:intl/src/intl/date_format.dart';
24 import 'package:mobx/mobx.dart';
@@ -71,6 +72,9 @@ abstract class TransactionDetailsViewModelBase with Store {
72 case WalletType.solana:
73 _addSolanaListItems(tx, dateFormat);
74 break;
75 + case WalletType.tron:
76 + _addTronListItems(tx, dateFormat);
77 + break;
78 default:
79 break;
80 }
@@ -160,6 +164,8 @@ abstract class TransactionDetailsViewModelBase with Store {
164 return 'https://polygonscan.com/tx/${txId}';
165 case WalletType.solana:
166 return 'https://solscan.io/tx/${txId}';
167 + case WalletType.tron:
168 + return 'https://tronscan.org/#/transaction/${txId}';
169 default:
170 return '';
171 }
@@ -186,6 +192,8 @@ abstract class TransactionDetailsViewModelBase with Store {
192 return S.current.view_transaction_on + 'polygonscan.com';
193 case WalletType.solana:
194 return S.current.view_transaction_on + 'solscan.io';
195 + case WalletType.tron:
196 + return S.current.view_transaction_on + 'tronscan.org';
197 default:
198 return '';
199 }
@@ -339,20 +347,19 @@ abstract class TransactionDetailsViewModelBase with Store {
347 transactionInfo.inputAddresses?.length ?? 1,
348 transactionInfo.outputAddresses?.length ?? 1);
349
342 - RBFListItems.add(StandartListItem(
343 - title: S.current.old_fee,
344 - value: tx.feeFormatted() ?? '0.0'));
350 + RBFListItems.add(StandartListItem(title: S.current.old_fee, value: tx.feeFormatted() ?? '0.0'));
351
352 final priorities = priorityForWalletType(wallet.type);
353 final selectedItem = priorities.indexOf(sendViewModel.transactionPriority);
348 - final customItem = priorities.firstWhereOrNull(
349 - (element) => element == sendViewModel.bitcoinTransactionPriorityCustom);
354 + final customItem = priorities
355 + .firstWhereOrNull((element) => element == sendViewModel.bitcoinTransactionPriorityCustom);
356 final customItemIndex = customItem != null ? priorities.indexOf(customItem) : null;
357 final maxCustomFeeRate = sendViewModel.maxCustomFeeRate?.toDouble();
358
359 RBFListItems.add(StandardPickerListItem(
360 title: S.current.estimated_new_fee,
355 - value: bitcoin!.formatterBitcoinAmountToString(amount: newFee) + ' ${walletTypeToCryptoCurrency(wallet.type)}',
361 + value: bitcoin!.formatterBitcoinAmountToString(amount: newFee) +
362 + ' ${walletTypeToCryptoCurrency(wallet.type)}',
363 items: priorityForWalletType(wallet.type),
364 customValue: settingsStore.customBitcoinFeeRate.toDouble(),
365 maxValue: maxCustomFeeRate,
@@ -378,6 +385,27 @@ abstract class TransactionDetailsViewModelBase with Store {
385 }
386 }
387
388 + void _addTronListItems(TransactionInfo tx, DateFormat dateFormat) {
389 + final _items = [
390 + StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id),
391 + StandartListItem(
392 + title: S.current.transaction_details_date, value: dateFormat.format(tx.date)),
393 + StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()),
394 + if (tx.feeFormatted()?.isNotEmpty ?? false)
395 + StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!),
396 + if (showRecipientAddress && tx.to != null)
397 + StandartListItem(
398 + title: S.current.transaction_details_recipient_address,
399 + value: tron!.getTronBase58Address(tx.to!, wallet)),
400 + if (tx.from != null)
401 + StandartListItem(
402 + title: S.current.transaction_details_source_address,
403 + value: tron!.getTronBase58Address(tx.from!, wallet)),
404 + ];
405 +
406 + items.addAll(_items);
407 + }
408 +
409 @action
410 Future<void> _checkForRBF() async {
411 if (wallet.type == WalletType.bitcoin &&
@@ -392,10 +420,10 @@ abstract class TransactionDetailsViewModelBase with Store {
420 newFee = priority == bitcoin!.getBitcoinTransactionPriorityCustom() && value != null
421 ? bitcoin!.getEstimatedFeeWithFeeRate(wallet, value.round(), transactionInfo.amount)
422 : bitcoin!.getFeeAmountForPriority(
395 - wallet,
396 - priority,
397 - transactionInfo.inputAddresses?.length ?? 1,
398 - transactionInfo.outputAddresses?.length ?? 1);
423 + wallet,
424 + priority,
425 + transactionInfo.inputAddresses?.length ?? 1,
426 + transactionInfo.outputAddresses?.length ?? 1);
427
428 return bitcoin!.formatterBitcoinAmountToString(amount: newFee);
429 }
lib/view_model/wallet_address_list/wallet_address_list_view_model.dart
+26
@@ -12,6 +12,7 @@ import 'package:cake_wallet/store/app_store.dart';
12 import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
13 import 'package:cake_wallet/store/settings_store.dart';
14 import 'package:cake_wallet/store/yat/yat_store.dart';
15 +import 'package:cake_wallet/tron/tron.dart';
16 import 'package:cake_wallet/utils/list_item.dart';
17 import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart';
18 import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart';
@@ -175,6 +176,21 @@ class SolanaURI extends PaymentURI {
176 }
177 }
178
179 +class TronURI extends PaymentURI {
180 + TronURI({required String amount, required String address})
181 + : super(amount: amount, address: address);
182 +
183 + @override
184 + String toString() {
185 + var base = 'tron:' + address;
186 + if (amount.isNotEmpty) {
187 + base += '?amount=${amount.replaceAll(',', '.')}';
188 + }
189 +
190 + return base;
191 + }
192 +}
193 +
194 abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store {
195 WalletAddressListViewModelBase({
196 required AppStore appStore,
@@ -273,6 +289,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
289 return SolanaURI(amount: amount, address: address.address);
290 }
291
292 + if (wallet.type == WalletType.tron) {
293 + return TronURI(amount: amount, address: address.address);
294 + }
295 +
296 throw Exception('Unexpected type: ${type.toString()}');
297 }
298
@@ -348,6 +368,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
368 addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress));
369 }
370
371 + if (wallet.type == WalletType.tron) {
372 + final primaryAddress = tron!.getAddress(wallet);
373 +
374 + addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress));
375 + }
376 +
377 if (searchText.isNotEmpty) {
378 return ObservableList.of(addressList.where((item) {
379 if (item is WalletAddressListItem) {
lib/view_model/wallet_keys_view_model.dart
+4 -1
@@ -118,7 +118,8 @@ abstract class WalletKeysViewModelBase with Store {
118 }
119
120 if (isEVMCompatibleChain(_appStore.wallet!.type) ||
121 - _appStore.wallet!.type == WalletType.solana) {
121 + _appStore.wallet!.type == WalletType.solana ||
122 + _appStore.wallet!.type == WalletType.tron) {
123 items.addAll([
124 if (_appStore.wallet!.privateKey != null)
125 StandartListItem(title: S.current.private_key, value: _appStore.wallet!.privateKey!),
@@ -175,6 +176,8 @@ abstract class WalletKeysViewModelBase with Store {
176 return 'polygon-wallet';
177 case WalletType.solana:
178 return 'solana-wallet';
179 + case WalletType.tron:
180 + return 'tron-wallet';
181 default:
182 throw Exception('Unexpected wallet type: ${_appStore.wallet!.toString()}');
183 }
lib/view_model/wallet_new_vm.dart
+4
@@ -1,6 +1,7 @@
1 import 'package:cake_wallet/ethereum/ethereum.dart';
2 import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
3 import 'package:cake_wallet/solana/solana.dart';
4 +import 'package:cake_wallet/tron/tron.dart';
5 import 'package:hive/hive.dart';
6 import 'package:mobx/mobx.dart';
7 import 'package:cake_wallet/monero/monero.dart';
@@ -43,6 +44,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
44 return 16;
45 }
46 return 25;
47 + case WalletType.tron:
48 case WalletType.solana:
49 case WalletType.polygon:
50 case WalletType.ethereum:
@@ -79,6 +81,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
81 return polygon!.createPolygonNewWalletCredentials(name: name);
82 case WalletType.solana:
83 return solana!.createSolanaNewWalletCredentials(name: name);
84 + case WalletType.tron:
85 + return tron!.createTronNewWalletCredentials(name: name);
86 default:
87 throw Exception('Unexpected type: ${type.toString()}');
88 }
lib/view_model/wallet_restore_view_model.dart
+16 -1
@@ -6,6 +6,7 @@ import 'package:cw_core/nano_account_info_response.dart';
6 import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
7 import 'package:cake_wallet/polygon/polygon.dart';
8 import 'package:cake_wallet/solana/solana.dart';
9 +import 'package:cake_wallet/tron/tron.dart';
10 import 'package:hive/hive.dart';
11 import 'package:mobx/mobx.dart';
12 import 'package:cake_wallet/store/app_store.dart';
@@ -34,7 +35,8 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
35 type == WalletType.polygon ||
36 type == WalletType.nano ||
37 type == WalletType.banano ||
37 - type == WalletType.solana,
38 + type == WalletType.solana ||
39 + type == WalletType.tron,
40 isButtonEnabled = false,
41 mode = WalletRestoreMode.seed,
42 super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true) {
@@ -48,6 +50,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
50 case WalletType.nano:
51 case WalletType.banano:
52 case WalletType.solana:
53 + case WalletType.tron:
54 availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys];
55 break;
56 default:
@@ -127,6 +130,12 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
130 mnemonic: seed,
131 password: password,
132 );
133 + case WalletType.tron:
134 + return tron!.createTronRestoreWalletFromSeedCredentials(
135 + name: name,
136 + mnemonic: seed,
137 + password: password,
138 + );
139 default:
140 break;
141 }
@@ -185,6 +194,12 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
194 password: password,
195 privateKey: options['private_key'] as String,
196 );
197 + case WalletType.tron:
198 + return tron!.createTronRestoreWalletFromPrivateKey(
199 + name: name,
200 + password: password,
201 + privateKey: options['private_key'] as String,
202 + );
203 default:
204 break;
205 }
model_generator.sh
+1
@@ -6,6 +6,7 @@ cd cw_haven && flutter pub get && flutter packages pub run build_runner build --
6 cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
7 cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
8 cd cw_solana && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
9 +cd cw_tron && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
10 cd cw_ethereum && flutter pub get && cd ..
11 cd cw_polygon && flutter pub get && cd ..
12 flutter packages pub run build_runner build --delete-conflicting-outputs
pubspec_base.yaml
+1
@@ -158,6 +158,7 @@ flutter:
158 - assets/nano_pow_node_list.yml
159 - assets/polygon_node_list.yml
160 - assets/solana_node_list.yml
161 + - assets/tron_node_list.yml
162 - assets/text/
163 - assets/faq/
164 - assets/animation/
scripts/android/pubspec_gen.sh
+1 -1
@@ -10,7 +10,7 @@ case $APP_ANDROID_TYPE in
10 CONFIG_ARGS="--monero"
11 ;;
12 $CAKEWALLET)
13 - CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana"
13 + CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana --tron"
14 ;;
15 $HAVEN)
16 CONFIG_ARGS="--haven"
scripts/ios/app_config.sh
+1 -1
@@ -28,7 +28,7 @@ case $APP_IOS_TYPE in
28 CONFIG_ARGS="--monero"
29 ;;
30 $CAKEWALLET)
31 - CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana"
31 + CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana --tron"
32 ;;
33 $HAVEN)
34
scripts/macos/app_config.sh
+1 -1
@@ -31,7 +31,7 @@ case $APP_MACOS_TYPE in
31 $MONERO_COM)
32 CONFIG_ARGS="--monero";;
33 $CAKEWALLET)
34 - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana";; #--haven
34 + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron";; #--haven
35 esac
36
37 cp -rf pubspec_description.yaml pubspec.yaml
tool/configure.dart
+94 -2
@@ -8,6 +8,7 @@ const bitcoinCashOutputPath = 'lib/bitcoin_cash/bitcoin_cash.dart';
8 const nanoOutputPath = 'lib/nano/nano.dart';
9 const polygonOutputPath = 'lib/polygon/polygon.dart';
10 const solanaOutputPath = 'lib/solana/solana.dart';
11 +const tronOutputPath = 'lib/tron/tron.dart';
12 const walletTypesPath = 'lib/wallet_types.g.dart';
13 const pubspecDefaultPath = 'pubspec_default.yaml';
14 const pubspecOutputPath = 'pubspec.yaml';
@@ -23,6 +24,7 @@ Future<void> main(List<String> args) async {
24 final hasBanano = args.contains('${prefix}banano');
25 final hasPolygon = args.contains('${prefix}polygon');
26 final hasSolana = args.contains('${prefix}solana');
27 + final hasTron = args.contains('${prefix}tron');
28
29 await generateBitcoin(hasBitcoin);
30 await generateMonero(hasMonero);
@@ -32,6 +34,7 @@ Future<void> main(List<String> args) async {
34 await generateNano(hasNano);
35 await generatePolygon(hasPolygon);
36 await generateSolana(hasSolana);
37 + await generateTron(hasTron);
38 // await generateBanano(hasEthereum);
39
40 await generatePubspec(
@@ -44,6 +47,7 @@ Future<void> main(List<String> args) async {
47 hasBitcoinCash: hasBitcoinCash,
48 hasPolygon: hasPolygon,
49 hasSolana: hasSolana,
50 + hasTron: hasTron,
51 );
52 await generateWalletTypes(
53 hasMonero: hasMonero,
@@ -55,6 +59,7 @@ Future<void> main(List<String> args) async {
59 hasBitcoinCash: hasBitcoinCash,
60 hasPolygon: hasPolygon,
61 hasSolana: hasSolana,
62 + hasTron: hasTron,
63 );
64 }
65
@@ -1024,6 +1029,79 @@ abstract class Solana {
1029 await outputFile.writeAsString(output);
1030 }
1031
1032 +Future<void> generateTron(bool hasImplementation) async {
1033 + final outputFile = File(tronOutputPath);
1034 + const tronCommonHeaders = """
1035 +import 'package:cake_wallet/view_model/send/output.dart';
1036 +import 'package:cw_core/crypto_currency.dart';
1037 +import 'package:cw_core/output_info.dart';
1038 +import 'package:cw_core/transaction_info.dart';
1039 +import 'package:cw_core/wallet_base.dart';
1040 +import 'package:cw_core/wallet_credentials.dart';
1041 +import 'package:cw_core/wallet_info.dart';
1042 +import 'package:cw_core/wallet_service.dart';
1043 +import 'package:hive/hive.dart';
1044 +
1045 +""";
1046 + const tronCWHeaders = """
1047 +import 'package:cw_evm/evm_chain_mnemonics.dart';
1048 +import 'package:cw_tron/tron_transaction_credentials.dart';
1049 +import 'package:cw_tron/tron_transaction_info.dart';
1050 +import 'package:cw_tron/tron_wallet_creation_credentials.dart';
1051 +
1052 +import 'package:cw_tron/tron_client.dart';
1053 +import 'package:cw_tron/tron_token.dart';
1054 +import 'package:cw_tron/tron_wallet.dart';
1055 +import 'package:cw_tron/tron_wallet_service.dart';
1056 +
1057 +""";
1058 + const tronCwPart = "part 'cw_tron.dart';";
1059 + const tronContent = """
1060 +abstract class Tron {
1061 + List<String> getTronWordList(String language);
1062 + WalletService createTronWalletService(Box<WalletInfo> walletInfoSource);
1063 + WalletCredentials createTronNewWalletCredentials({required String name, WalletInfo? walletInfo});
1064 + WalletCredentials createTronRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password});
1065 + WalletCredentials createTronRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password});
1066 + String getAddress(WalletBase wallet);
1067 +
1068 + Object createTronTransactionCredentials(
1069 + List<Output> outputs, {
1070 + required CryptoCurrency currency,
1071 + });
1072 +
1073 + List<CryptoCurrency> getTronTokenCurrencies(WalletBase wallet);
1074 + Future<void> addTronToken(WalletBase wallet, CryptoCurrency token, String contractAddress);
1075 + Future<void> deleteTronToken(WalletBase wallet, CryptoCurrency token);
1076 + Future<CryptoCurrency?> getTronToken(WalletBase wallet, String contractAddress);
1077 +
1078 + double getTransactionAmountRaw(TransactionInfo transactionInfo);
1079 + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction);
1080 + String getTokenAddress(CryptoCurrency asset);
1081 + String getTronBase58Address(String hexAddress, WalletBase wallet);
1082 +
1083 + String? getTronNativeEstimatedFee(WalletBase wallet);
1084 + String? getTronTRC20EstimatedFee(WalletBase wallet);
1085 +}
1086 + """;
1087 +
1088 + const tronEmptyDefinition = 'Tron? tron;\n';
1089 + const tronCWDefinition = 'Tron? tron = CWTron();\n';
1090 +
1091 + final output = '$tronCommonHeaders\n' +
1092 + (hasImplementation ? '$tronCWHeaders\n' : '\n') +
1093 + (hasImplementation ? '$tronCwPart\n\n' : '\n') +
1094 + (hasImplementation ? tronCWDefinition : tronEmptyDefinition) +
1095 + '\n' +
1096 + tronContent;
1097 +
1098 + if (outputFile.existsSync()) {
1099 + await outputFile.delete();
1100 + }
1101 +
1102 + await outputFile.writeAsString(output);
1103 +}
1104 +
1105 Future<void> generatePubspec(
1106 {required bool hasMonero,
1107 required bool hasBitcoin,
@@ -1033,7 +1111,8 @@ Future<void> generatePubspec(
1111 required bool hasBanano,
1112 required bool hasBitcoinCash,
1113 required bool hasPolygon,
1036 - required bool hasSolana}) async {
1114 + required bool hasSolana,
1115 + required bool hasTron}) async {
1116 const cwCore = """
1117 cw_core:
1118 path: ./cw_core
@@ -1082,6 +1161,10 @@ Future<void> generatePubspec(
1161 cw_evm:
1162 path: ./cw_evm
1163 """;
1164 + const cwTron = """
1165 + cw_tron:
1166 + path: ./cw_tron
1167 + """;
1168 final inputFile = File(pubspecOutputPath);
1169 final inputText = await inputFile.readAsString();
1170 final inputLines = inputText.split('\n');
@@ -1121,6 +1204,10 @@ Future<void> generatePubspec(
1204 output += '\n$cwSolana';
1205 }
1206
1207 + if (hasTron) {
1208 + output += '\n$cwTron';
1209 + }
1210 +
1211 if (hasHaven && !hasMonero) {
1212 output += '\n$cwSharedExternal\n$cwHaven';
1213 } else if (hasHaven) {
@@ -1152,7 +1239,8 @@ Future<void> generateWalletTypes(
1239 required bool hasBanano,
1240 required bool hasBitcoinCash,
1241 required bool hasPolygon,
1155 - required bool hasSolana}) async {
1242 + required bool hasSolana,
1243 + required bool hasTron}) async {
1244 final walletTypesFile = File(walletTypesPath);
1245
1246 if (walletTypesFile.existsSync()) {
@@ -1191,6 +1279,10 @@ Future<void> generateWalletTypes(
1279 outputContent += '\tWalletType.solana,\n';
1280 }
1281
1282 + if (hasTron) {
1283 + outputContent += '\tWalletType.tron,\n';
1284 + }
1285 +
1286 if (hasNano) {
1287 outputContent += '\tWalletType.nano,\n';
1288 }
tool/generate_secrets_config.dart
+17 -1
@@ -6,6 +6,7 @@ import 'utils/utils.dart';
6 const configPath = 'tool/.secrets-config.json';
7 const evmChainsConfigPath = 'tool/.evm-secrets-config.json';
8 const solanaConfigPath = 'tool/.solana-secrets-config.json';
9 +const tronConfigPath = 'tool/.tron-secrets-config.json';
10
11 Future<void> main(List<String> args) async => generateSecretsConfig(args);
12
@@ -20,9 +21,10 @@ Future<void> generateSecretsConfig(List<String> args) async {
21 final configFile = File(configPath);
22 final evmChainsConfigFile = File(evmChainsConfigPath);
23 final solanaConfigFile = File(solanaConfigPath);
24 + final tronConfigFile = File(tronConfigPath);
25
26 final secrets = <String, dynamic>{};
25 -
27 +
28 secrets.addAll(extraInfo);
29 secrets.removeWhere((key, dynamic value) {
30 if (key.contains('--')) {
@@ -78,4 +80,18 @@ Future<void> generateSecretsConfig(List<String> args) async {
80 secretsJson = JsonEncoder.withIndent(' ').convert(secrets);
81
82 await solanaConfigFile.writeAsString(secretsJson);
83 +
84 + secrets.clear();
85 +
86 + SecretKey.tronSecrets.forEach((sec) {
87 + if (secrets[sec.name] != null) {
88 + return;
89 + }
90 +
91 + secrets[sec.name] = sec.generate();
92 + });
93 +
94 + secretsJson = JsonEncoder.withIndent(' ').convert(secrets);
95 +
96 + await tronConfigFile.writeAsString(secretsJson);
97 }
tool/import_secrets_config.dart
+14
@@ -10,6 +10,9 @@ const evmChainsOutputPath = 'cw_evm/lib/.secrets.g.dart';
10
11 const solanaConfigPath = 'tool/.solana-secrets-config.json';
12 const solanaOutputPath = 'cw_solana/lib/.secrets.g.dart';
13 +
14 +const tronConfigPath = 'tool/.tron-secrets-config.json';
15 +const tronOutputPath = 'cw_tron/lib/.secrets.g.dart';
16 Future<void> main(List<String> args) async => importSecretsConfig();
17
18 Future<void> importSecretsConfig() async {
@@ -29,6 +32,11 @@ Future<void> importSecretsConfig() async {
32 final solanaOutput =
33 solanaInput.keys.fold('', (String acc, String val) => acc + generateConst(val, solanaInput));
34
35 + final tronOutputFile = File(tronOutputPath);
36 + final tronInput = json.decode(File(tronConfigPath).readAsStringSync()) as Map<String, dynamic>;
37 + final tronOutput =
38 + tronInput.keys.fold('', (String acc, String val) => acc + generateConst(val, tronInput));
39 +
40 if (outputFile.existsSync()) {
41 await outputFile.delete();
42 }
@@ -46,4 +54,10 @@ Future<void> importSecretsConfig() async {
54 }
55
56 await solanaOutputFile.writeAsString(solanaOutput);
57 +
58 + if (tronOutputFile.existsSync()) {
59 + await tronOutputFile.delete();
60 + }
61 +
62 + await tronOutputFile.writeAsString(tronOutput);
63 }
tool/utils/secret_key.dart
+4
@@ -50,6 +50,10 @@ class SecretKey {
50 SecretKey('ankrApiKey', () => ''),
51 ];
52
53 + static final tronSecrets = [
54 + SecretKey('tronGridApiKey', () => ''),
55 + ];
56 +
57 final String name;
58 final String Function() generate;
59 }