CW-555-Add-Solana-Wallet (#1272)

* chore: Create cw_solana package and clean up files * feat: Add Solana Wallet - Create, Restore form seed, restore from Key, Restore from QR, Send, Receive, transaction history, spl tokens * fix: Make transactions file specific to solana only for solana transactions * chore: Revert inject app details script * fix: Fix issue with node and switch current node to main beta instead of testnet * fix: Fix merge conflicts and adjust migration version * fix: Fetch spl token error Signed-off-by: Blazebrain <davidadegoke16@gmail.com> * fix: Diplay and activate spl tokens bug * fix: Review and fixes * fix: reverted formatting for cryptocurrency class * fix: Review comments, split sending flow into signing and sending separately, fix issues * fix: Revert throwing unimplenented error * chore: Fix comment * chore: Fix comment * fix: Errors in flow * Update provider_types.dart [skip ci] * fix: Issues with solana wallet * Update solana_wallet.dart [skip ci] * fix: Review comments * fix: Date time config * fix: Revert bash script for app details * fix: Error with balance, displaying fees, fixing sent or received identifier bug, displaying token symbol with token transaction item in transactions list * fix: Issues with address validation when sending spl tokens and walletconnect initial setup * fix: Issues with sending, fetching transactions history, almost wrapping up walletconnect * fix: Adjust imports that would affect monerocom building successfully * fix: Refine transaction direction and continue work on walletconnect * feat: Display SPL token transfers in the transaction history and finally settle the transaction direction * fix: Delay in transactions history dispaly, show native token transactions first, then process spl token transactions * feat: Switch node and revert solana chain id to previous id * fix: Remove print statement * fix: Remove await for transactions, fetch all transaction histories instantly and adjust solana send success message * chore: Code refactoring and streamlined wallet type check for solana send success message * fix: Make timeout error for node silent and add spl token images --------- Signed-off-by: Blazebrain <davidadegoke16@gmail.com> Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

Adegoke David committed Feb 23, 2024 at 14:39 UTC 109bba43013b90270bb556d954a34102ec7a4c30
133 files changed +5354 -351
.github/workflows/pr_test_build.yml
+17 -14
@@ -6,9 +6,9 @@ on:
6 workflow_dispatch:
7 inputs:
8 branch:
9 - description: 'Branch name to build'
9 + description: "Branch name to build"
10 required: true
11 - default: 'main'
11 + default: "main"
12
13 jobs:
14 PR_test_build:
@@ -111,6 +111,7 @@ jobs:
111 cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
112 cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
113 cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
114 + cd cw_solana && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
115 cd cw_ethereum && flutter pub get && cd ..
116 cd cw_polygon && flutter pub get && cd ..
117 flutter packages pub run build_runner build --delete-conflicting-outputs
@@ -120,6 +121,7 @@ jobs:
121 cd /opt/android/cake_wallet
122 touch lib/.secrets.g.dart
123 touch cw_evm/lib/.secrets.g.dart
124 + touch cw_solana/lib/.secrets.g.dart
125 echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart
126 echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart
127 echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart
@@ -154,6 +156,7 @@ jobs:
156 echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart
157 echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart
158 echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
159 + echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
160
161 - name: Rename app
162 run: echo -e "id=com.cakewallet.test\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties
@@ -163,18 +166,18 @@ jobs:
166 cd /opt/android/cake_wallet
167 flutter build apk --release
168
166 -# - name: Push to App Center
167 -# run: |
168 -# echo 'Installing App Center CLI tools'
169 -# npm install -g appcenter-cli
170 -# echo "Publishing test to App Center"
171 -# appcenter distribute release \
172 -# --group "Testers" \
173 -# --file "/opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk" \
174 -# --release-notes ${{ env.BRANCH_NAME }} \
175 -# --app Cake-Labs/Cake-Wallet \
176 -# --token ${{ secrets.APP_CENTER_TOKEN }} \
177 -# --quiet
169 + # - name: Push to App Center
170 + # run: |
171 + # echo 'Installing App Center CLI tools'
172 + # npm install -g appcenter-cli
173 + # echo "Publishing test to App Center"
174 + # appcenter distribute release \
175 + # --group "Testers" \
176 + # --file "/opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk" \
177 + # --release-notes ${{ env.BRANCH_NAME }} \
178 + # --app Cake-Labs/Cake-Wallet \
179 + # --token ${{ secrets.APP_CENTER_TOKEN }} \
180 + # --quiet
181
182 - name: Rename apk file
183 run: |
.gitignore
+3
@@ -92,8 +92,10 @@ android/key.properties
92 **/tool/.secrets-config.json
93 **/tool/.evm-secrets-config.json
94 **/tool/.ethereum-secrets-config.json
95 +**/tool/.solana-secrets-config.json
96 **/lib/.secrets.g.dart
97 **/cw_evm/lib/.secrets.g.dart
98 +**/cw_solana/lib/.secrets.g.dart
99
100 vendor/
101
@@ -128,6 +130,7 @@ lib/ethereum/ethereum.dart
130 lib/bitcoin_cash/bitcoin_cash.dart
131 lib/nano/nano.dart
132 lib/polygon/polygon.dart
133 +lib/solana/solana.dart
134
135 ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png
136 ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png
android/app/src/main/AndroidManifestBase.xml
+1
@@ -66,6 +66,7 @@
66 <data android:scheme="polygon" />
67 <data android:scheme="polygon-wallet" />
68 <data android:scheme="polygon_wallet" />
69 + <data android:scheme="solana-wallet" />
70 </intent-filter>
71 </activity>
72 <meta-data
assets/images/avdo_icon.png
Binary files /dev/null and b/assets/images/avdo_icon.png differ
assets/images/bonk_icon.png
Binary files /dev/null and b/assets/images/bonk_icon.png differ
assets/images/gmt_icon.png
Binary files /dev/null and b/assets/images/gmt_icon.png differ
assets/images/hnt_icon.png
Binary files /dev/null and b/assets/images/hnt_icon.png differ
assets/images/ray_icon.png
Binary files /dev/null and b/assets/images/ray_icon.png differ
assets/solana_node_list.yml new
+4
@@ -0,0 +1,4 @@
1 +-
2 + uri: rpc.ankr.com
3 + is_default: true
4 + useSSL: true
\ No newline at end of file
cw_core/lib/crypto_currency.dart
+6 -1
@@ -9,7 +9,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
9 required this.decimals,
10 this.fullName,
11 this.iconPath,
12 - this.tag})
12 + this.tag, this.enabled = false,
13 + })
14 : super(title: title, raw: raw);
15
16 final String name;
@@ -17,6 +18,9 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
18 final String? fullName;
19 final String? iconPath;
20 final int decimals;
21 + final bool enabled;
22 +
23 + set enabled(bool value) => this.enabled = value;
24
25 static const all = [
26 CryptoCurrency.xmr,
@@ -208,6 +212,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
212 static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POLY', fullName: 'Tether USD (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/usdt_icon.png', decimals: 6);
213 static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POLY', fullName: 'USD Coin (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6);
214 static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kaspa', iconPath: 'assets/images/kaspa_icon.png', decimals: 8);
215 + static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'USDT Tether', raw: 90, name: 'usdtsol', iconPath: 'assets/images/usdt_icon.png', decimals: 6);
216
217
218 static final Map<int, CryptoCurrency> _rawCurrencyMap =
cw_core/lib/currency_for_wallet_type.dart
+2
@@ -21,6 +21,8 @@ CryptoCurrency currencyForWalletType(WalletType type) {
21 return CryptoCurrency.banano;
22 case WalletType.polygon:
23 return CryptoCurrency.maticpoly;
24 + case WalletType.solana:
25 + return CryptoCurrency.sol;
26 default:
27 throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType');
28 }
cw_core/lib/hive_type_ids.dart
+2 -1
@@ -13,4 +13,5 @@ const ADDRESS_INFO_TYPE_ID = 11;
13 const ERC20_TOKEN_TYPE_ID = 12;
14 const NANO_ACCOUNT_TYPE_ID = 13;
15 const POW_NODE_TYPE_ID = 14;
16 -const DERIVATION_TYPE_TYPE_ID = 15;
\ No newline at end of file
16 +const DERIVATION_TYPE_TYPE_ID = 15;
17 +const SPL_TOKEN_TYPE_ID = 16;
cw_core/lib/node.dart
+8 -15
@@ -70,15 +70,10 @@ class Node extends HiveObject with Keyable {
70 Uri get uri {
71 switch (type) {
72 case WalletType.monero:
73 + case WalletType.haven:
74 return Uri.http(uriRaw, '');
75 case WalletType.bitcoin:
75 - return createUriFromElectrumAddress(uriRaw);
76 case WalletType.litecoin:
77 - return createUriFromElectrumAddress(uriRaw);
78 - case WalletType.haven:
79 - return Uri.http(uriRaw, '');
80 - case WalletType.ethereum:
81 - return Uri.https(uriRaw, '');
77 case WalletType.bitcoinCash:
78 return createUriFromElectrumAddress(uriRaw);
79 case WalletType.nano:
@@ -88,7 +83,9 @@ class Node extends HiveObject with Keyable {
83 } else {
84 return Uri.http(uriRaw, '');
85 }
86 + case WalletType.ethereum:
87 case WalletType.polygon:
88 + case WalletType.solana:
89 return Uri.https(uriRaw, '');
90 default:
91 throw Exception('Unexpected type ${type.toString()} for Node uri');
@@ -134,21 +131,17 @@ class Node extends HiveObject with Keyable {
131 try {
132 switch (type) {
133 case WalletType.monero:
137 - return requestMoneroNode();
138 - case WalletType.bitcoin:
139 - return requestElectrumServer();
140 - case WalletType.litecoin:
141 - return requestElectrumServer();
134 case WalletType.haven:
135 return requestMoneroNode();
144 - case WalletType.ethereum:
145 - return requestElectrumServer();
146 - case WalletType.bitcoinCash:
147 - return requestElectrumServer();
136 case WalletType.nano:
137 case WalletType.banano:
138 return requestNanoNode();
139 + case WalletType.bitcoin:
140 + case WalletType.litecoin:
141 + case WalletType.bitcoinCash:
142 + case WalletType.ethereum:
143 case WalletType.polygon:
144 + case WalletType.solana:
145 return requestElectrumServer();
146 default:
147 return false;
cw_core/lib/pathForWallet.dart
-1
@@ -1,6 +1,5 @@
1 import 'dart:io';
2 import 'package:cw_core/wallet_type.dart';
3 -import 'package:flutter/foundation.dart';
3 import 'package:path_provider/path_provider.dart';
4
5 Future<String> pathForWalletDir({required String name, required WalletType type}) async {
cw_core/lib/wallet_type.dart
+15 -1
@@ -14,6 +14,7 @@ const walletTypes = [
14 WalletType.nano,
15 WalletType.banano,
16 WalletType.polygon,
17 + WalletType.solana,
18 ];
19
20 @HiveType(typeId: WALLET_TYPE_TYPE_ID)
@@ -46,7 +47,10 @@ enum WalletType {
47 bitcoinCash,
48
49 @HiveField(9)
49 - polygon
50 + polygon,
51 +
52 + @HiveField(10)
53 + solana
54 }
55
56 int serializeToInt(WalletType type) {
@@ -69,6 +73,8 @@ int serializeToInt(WalletType type) {
73 return 7;
74 case WalletType.polygon:
75 return 8;
76 + case WalletType.solana:
77 + return 9;
78 default:
79 return -1;
80 }
@@ -94,6 +100,8 @@ WalletType deserializeFromInt(int raw) {
100 return WalletType.bitcoinCash;
101 case 8:
102 return WalletType.polygon;
103 + case 9:
104 + return WalletType.solana;
105 default:
106 throw Exception('Unexpected token: $raw for WalletType deserializeFromInt');
107 }
@@ -119,6 +127,8 @@ String walletTypeToString(WalletType type) {
127 return 'Banano';
128 case WalletType.polygon:
129 return 'Polygon';
130 + case WalletType.solana:
131 + return 'Solana';
132 default:
133 return '';
134 }
@@ -144,6 +154,8 @@ String walletTypeToDisplayName(WalletType type) {
154 return 'Banano (BAN)';
155 case WalletType.polygon:
156 return 'Polygon (MATIC)';
157 + case WalletType.solana:
158 + return 'Solana (SOL)';
159 default:
160 return '';
161 }
@@ -169,6 +181,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) {
181 return CryptoCurrency.banano;
182 case WalletType.polygon:
183 return CryptoCurrency.maticpoly;
184 + case WalletType.solana:
185 + return CryptoCurrency.sol;
186 default:
187 throw Exception(
188 'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency');
cw_solana/.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_solana/.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_solana/CHANGELOG.md new
+3
@@ -0,0 +1,3 @@
1 +## 0.0.1
2 +
3 +* TODO: Describe initial release.
cw_solana/LICENSE new
+1
@@ -0,0 +1 @@
1 +TODO: Add your license here.
cw_solana/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_solana/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_solana/lib/cw_solana.dart new
+7
@@ -0,0 +1,7 @@
1 +library cw_solana;
2 +
3 +/// A Calculator.
4 +class Calculator {
5 + /// Returns [value] plus 1.
6 + int addOne(int value) => value + 1;
7 +}
cw_solana/lib/default_spl_tokens.dart new
+109
@@ -0,0 +1,109 @@
1 +import 'package:cw_core/crypto_currency.dart';
2 +import 'package:cw_solana/spl_token.dart';
3 +
4 +class DefaultSPLTokens {
5 + final List<SPLToken> _defaultTokens = [
6 + SPLToken(
7 + name: 'USDT Tether',
8 + symbol: 'USDT',
9 + mintAddress: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
10 + decimal: 6,
11 + mint: 'usdtsol',
12 + enabled: true,
13 + ),
14 + SPLToken(
15 + name: 'USD Coin',
16 + symbol: 'USDC',
17 + mintAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
18 + decimal: 6,
19 + mint: 'usdcsol',
20 + enabled: true,
21 + ),
22 + SPLToken(
23 + name: 'Wrapped Ethereum (Sollet)',
24 + symbol: 'soETH',
25 + mintAddress: '2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk',
26 + decimal: 6,
27 + mint: 'soEth',
28 + enabled: true,
29 + iconPath: 'assets/images/eth_icon.png',
30 + ),
31 + SPLToken(
32 + name: 'Wrapped SOL',
33 + symbol: 'WSOL',
34 + mintAddress: 'So11111111111111111111111111111111111111112',
35 + decimal: 9,
36 + mint: 'WSOL',
37 + enabled: true,
38 + iconPath: 'assets/images/sol_icon.png',
39 + ),
40 + SPLToken(
41 + name: 'Wrapped Bitcoin (Sollet)',
42 + symbol: 'BTC',
43 + mintAddress: '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E',
44 + decimal: 6,
45 + mint: 'btcsol',
46 + iconPath: 'assets/images/btc.png',
47 + ),
48 + SPLToken(
49 + name: 'Bonk',
50 + symbol: 'Bonk',
51 + mintAddress: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
52 + decimal: 5,
53 + mint: 'Bonk',
54 + iconPath: 'assets/images/bonk_icon.png',
55 + ),
56 + SPLToken(
57 + name: 'Helium Network Token',
58 + symbol: 'HNT',
59 + mintAddress: 'hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux',
60 + decimal: 8,
61 + mint: 'hnt',
62 + iconPath: 'assets/images/hnt_icon.png',
63 + ),
64 + SPLToken(
65 + name: 'Pyth Network',
66 + symbol: 'PYTH',
67 + mintAddress: 'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3',
68 + decimal: 6,
69 + mint: 'pyth',
70 + ),
71 + SPLToken(
72 + name: 'Raydium',
73 + symbol: 'RAY',
74 + mintAddress: '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R',
75 + decimal: 6,
76 + mint: 'ray',
77 + iconPath: 'assets/images/ray_icon.png',
78 + ),
79 + SPLToken(
80 + name: 'GMT',
81 + symbol: 'GMT',
82 + mintAddress: '7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx',
83 + decimal: 6,
84 + mint: 'ray',
85 + iconPath: 'assets/images/gmt_icon.png',
86 + ),
87 + SPLToken(
88 + name: 'AvocadoCoin',
89 + symbol: 'AVDO',
90 + mintAddress: 'EE5L8cMU4itTsCSuor7NLK6RZx6JhsBe8GGV3oaAHm3P',
91 + decimal: 8,
92 + mint: 'avdo',
93 + iconPath: 'assets/images/avdo_icon.png',
94 + ),
95 + ];
96 +
97 + List<SPLToken> get initialSPLTokens => _defaultTokens.map((token) {
98 + String? iconPath;
99 + if (token.iconPath != null) return token;
100 +
101 + try {
102 + iconPath = CryptoCurrency.all
103 + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
104 + .iconPath;
105 + } catch (_) {}
106 +
107 + return SPLToken.copyWith(token, iconPath, 'SOL');
108 + }).toList();
109 +}
cw_solana/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_solana/lib/pending_solana_transaction.dart new
+43
@@ -0,0 +1,43 @@
1 +import 'package:cw_core/pending_transaction.dart';
2 +import 'package:solana/encoder.dart';
3 +
4 +class PendingSolanaTransaction with PendingTransaction {
5 + final double amount;
6 + final SignedTx signedTransaction;
7 + final String destinationAddress;
8 + final Function sendTransaction;
9 + final double fee;
10 +
11 + PendingSolanaTransaction({
12 + required this.fee,
13 + required this.amount,
14 + required this.signedTransaction,
15 + required this.destinationAddress,
16 + required this.sendTransaction,
17 + });
18 +
19 + @override
20 + String get amountFormatted {
21 + String stringifiedAmount = amount.toString();
22 +
23 + if (stringifiedAmount.toString().length >= 6) {
24 + stringifiedAmount = stringifiedAmount.substring(0, 6);
25 + }
26 +
27 + return stringifiedAmount;
28 + }
29 +
30 + @override
31 + Future<void> commit() async {
32 + return await sendTransaction();
33 + }
34 +
35 + @override
36 + String get feeFormatted => fee.toString();
37 +
38 + @override
39 + String get hex => signedTransaction.encode();
40 +
41 + @override
42 + String get id => '';
43 +}
cw_solana/lib/solana_balance.dart new
+39
@@ -0,0 +1,39 @@
1 +import 'dart:convert';
2 +
3 +import 'package:cw_core/balance.dart';
4 +
5 +class SolanaBalance extends Balance {
6 + SolanaBalance(this.balance) : super(balance.toInt(), balance.toInt());
7 +
8 + final double balance;
9 +
10 + @override
11 + String get formattedAdditionalBalance => _balanceFormatted();
12 +
13 + @override
14 + String get formattedAvailableBalance => _balanceFormatted();
15 +
16 + String _balanceFormatted() {
17 + String stringBalance = balance.toString();
18 + if (stringBalance.toString().length >= 6) {
19 + stringBalance = stringBalance.substring(0, 6);
20 + }
21 + return stringBalance;
22 + }
23 +
24 + static SolanaBalance? fromJSON(String? jsonSource) {
25 + if (jsonSource == null) {
26 + return null;
27 + }
28 +
29 + final decoded = json.decode(jsonSource) as Map;
30 +
31 + try {
32 + return SolanaBalance(decoded['balance']);
33 + } catch (e) {
34 + return SolanaBalance(0.0);
35 + }
36 + }
37 +
38 + String toJSON() => json.encode({'balance': balance.toString()});
39 +}
cw_solana/lib/solana_client.dart new
+477
@@ -0,0 +1,477 @@
1 +import 'dart:async';
2 +import 'dart:convert';
3 +import 'dart:math';
4 +
5 +import 'package:cw_core/crypto_currency.dart';
6 +import 'package:cw_core/node.dart';
7 +import 'package:cw_solana/pending_solana_transaction.dart';
8 +import 'package:cw_solana/solana_balance.dart';
9 +import 'package:cw_solana/solana_transaction_model.dart';
10 +import 'package:http/http.dart' as http;
11 +import 'package:solana/dto.dart';
12 +import 'package:solana/encoder.dart';
13 +import 'package:solana/solana.dart';
14 +import '.secrets.g.dart' as secrets;
15 +
16 +class SolanaWalletClient {
17 + final httpClient = http.Client();
18 + SolanaClient? _client;
19 +
20 + bool connect(Node node) {
21 + try {
22 + Uri? rpcUri;
23 + String webSocketUrl;
24 + bool isModifiedNodeUri = false;
25 +
26 + if (node.uriRaw == 'rpc.ankr.com') {
27 + isModifiedNodeUri = true;
28 + String ankrApiKey = secrets.ankrApiKey;
29 +
30 + rpcUri = Uri.https(node.uriRaw, '/solana/$ankrApiKey');
31 + webSocketUrl = 'wss://${node.uriRaw}/solana/ws/$ankrApiKey';
32 + } else {
33 + webSocketUrl = 'wss://${node.uriRaw}';
34 + }
35 +
36 + _client = SolanaClient(
37 + rpcUrl: isModifiedNodeUri ? rpcUri! : node.uri,
38 + websocketUrl: Uri.parse(webSocketUrl),
39 + timeout: const Duration(minutes: 2),
40 + );
41 + return true;
42 + } catch (e) {
43 + return false;
44 + }
45 + }
46 +
47 + Future<double> getBalance(String address) async {
48 + try {
49 + final balance = await _client!.rpcClient.getBalance(address);
50 +
51 + final solBalance = balance.value / lamportsPerSol;
52 +
53 + return solBalance;
54 + } catch (_) {
55 + return 0.0;
56 + }
57 + }
58 +
59 + Future<ProgramAccountsResult?> getSPLTokenAccounts(String mintAddress, String publicKey) async {
60 + try {
61 + final tokenAccounts = await _client!.rpcClient.getTokenAccountsByOwner(
62 + publicKey,
63 + TokenAccountsFilter.byMint(mintAddress),
64 + commitment: Commitment.confirmed,
65 + encoding: Encoding.jsonParsed,
66 + );
67 + return tokenAccounts;
68 + } catch (e) {
69 + return null;
70 + }
71 + }
72 +
73 + Future<SolanaBalance?> getSplTokenBalance(String mintAddress, String publicKey) async {
74 + // Fetch the token accounts (a token can have multiple accounts for various uses)
75 + final tokenAccounts = await getSPLTokenAccounts(mintAddress, publicKey);
76 +
77 + // Handle scenario where there is no token account
78 + if (tokenAccounts == null || tokenAccounts.value.isEmpty) {
79 + return null;
80 + }
81 +
82 + // Sum the balances of all accounts with the specified mint address
83 + double totalBalance = 0.0;
84 +
85 + for (var programAccount in tokenAccounts.value) {
86 + final tokenAmountResult =
87 + await _client!.rpcClient.getTokenAccountBalance(programAccount.pubkey);
88 +
89 + final balance = tokenAmountResult.value.uiAmountString;
90 +
91 + final balanceAsDouble = double.tryParse(balance ?? '0.0') ?? 0.0;
92 +
93 + totalBalance += balanceAsDouble;
94 + }
95 +
96 + return SolanaBalance(totalBalance);
97 + }
98 +
99 + Future<double> getGasForMessage(String message) async {
100 + try {
101 + final gasPrice = await _client!.rpcClient.getFeeForMessage(message) ?? 0;
102 + final fee = gasPrice / lamportsPerSol;
103 + return fee;
104 + } catch (_) {
105 + return 0;
106 + }
107 + }
108 +
109 + /// Load the Address's transactions into the account
110 + Future<List<SolanaTransactionModel>> fetchTransactions(
111 + Ed25519HDPublicKey publicKey, {
112 + String? splTokenSymbol,
113 + int? splTokenDecimal,
114 + }) async {
115 + List<SolanaTransactionModel> transactions = [];
116 +
117 + try {
118 + final response = await _client!.rpcClient.getTransactionsList(
119 + publicKey,
120 + commitment: Commitment.confirmed,
121 + limit: 1000,
122 + );
123 +
124 + for (final tx in response) {
125 + if (tx.transaction is ParsedTransaction) {
126 + final parsedTx = (tx.transaction as ParsedTransaction);
127 + final message = parsedTx.message;
128 +
129 + final fee = (tx.meta?.fee ?? 0) / lamportsPerSol;
130 +
131 + for (final instruction in message.instructions) {
132 + if (instruction is ParsedInstruction) {
133 + instruction.map(
134 + system: (systemData) {
135 + systemData.parsed.map(
136 + transfer: (transferData) {
137 + ParsedSystemTransferInformation transfer = transferData.info;
138 + bool isOutgoingTx = transfer.source == publicKey.toBase58();
139 +
140 + double amount = transfer.lamports.toDouble() / lamportsPerSol;
141 +
142 + transactions.add(
143 + SolanaTransactionModel(
144 + id: parsedTx.signatures.first,
145 + from: transfer.source,
146 + to: transfer.destination,
147 + amount: amount,
148 + isOutgoingTx: isOutgoingTx,
149 + blockTimeInInt: tx.blockTime!,
150 + fee: fee,
151 + programId: SystemProgram.programId,
152 + tokenSymbol: 'SOL',
153 + ),
154 + );
155 + },
156 + transferChecked: (_) {},
157 + unsupported: (_) {},
158 + );
159 + },
160 + splToken: (splTokenData) {
161 + if (splTokenSymbol != null) {
162 + splTokenData.parsed.map(
163 + transfer: (transferData) {
164 + SplTokenTransferInfo transfer = transferData.info;
165 + bool isOutgoingTx = transfer.source == publicKey.toBase58();
166 +
167 + double amount = (double.tryParse(transfer.amount) ?? 0.0) /
168 + pow(10, splTokenDecimal ?? 9);
169 +
170 + transactions.add(
171 + SolanaTransactionModel(
172 + id: parsedTx.signatures.first,
173 + fee: fee,
174 + from: transfer.source,
175 + to: transfer.destination,
176 + amount: amount,
177 + isOutgoingTx: isOutgoingTx,
178 + programId: TokenProgram.programId,
179 + blockTimeInInt: tx.blockTime!,
180 + tokenSymbol: splTokenSymbol,
181 + ),
182 + );
183 + },
184 + transferChecked: (transferCheckedData) {
185 + SplTokenTransferCheckedInfo transfer = transferCheckedData.info;
186 + bool isOutgoingTx = transfer.source == publicKey.toBase58();
187 + double amount =
188 + double.tryParse(transfer.tokenAmount.uiAmountString ?? '0.0') ?? 0.0;
189 +
190 + transactions.add(
191 + SolanaTransactionModel(
192 + id: parsedTx.signatures.first,
193 + fee: fee,
194 + from: transfer.source,
195 + to: transfer.destination,
196 + amount: amount,
197 + isOutgoingTx: isOutgoingTx,
198 + programId: TokenProgram.programId,
199 + blockTimeInInt: tx.blockTime!,
200 + tokenSymbol: splTokenSymbol,
201 + ),
202 + );
203 + },
204 + generic: (genericData) {},
205 + );
206 + }
207 + },
208 + memo: (_) {},
209 + unsupported: (a) {},
210 + );
211 + }
212 + }
213 + }
214 + }
215 +
216 + return transactions;
217 + } catch (err) {
218 + return [];
219 + }
220 + }
221 +
222 + Future<List<SolanaTransactionModel>> getSPLTokenTransfers(
223 + String address,
224 + String splTokenSymbol,
225 + int splTokenDecimal,
226 + Ed25519HDKeyPair ownerKeypair,
227 + ) async {
228 + final tokenMint = Ed25519HDPublicKey.fromBase58(address);
229 +
230 + ProgramAccount? associatedTokenAccount;
231 +
232 + try {
233 + associatedTokenAccount = await _client!.getAssociatedTokenAccount(
234 + mint: tokenMint,
235 + owner: ownerKeypair.publicKey,
236 + commitment: Commitment.confirmed,
237 + );
238 + } catch (_) {}
239 +
240 + if (associatedTokenAccount == null) return [];
241 +
242 + final accountPublicKey = Ed25519HDPublicKey.fromBase58(associatedTokenAccount.pubkey);
243 +
244 + final tokenTransactions = await fetchTransactions(
245 + accountPublicKey,
246 + splTokenSymbol: splTokenSymbol,
247 + splTokenDecimal: splTokenDecimal,
248 + );
249 +
250 + return tokenTransactions;
251 + }
252 +
253 + void stop() {}
254 +
255 + SolanaClient? get getSolanaClient => _client;
256 +
257 + Future<PendingSolanaTransaction> signSolanaTransaction({
258 + required String tokenTitle,
259 + required int tokenDecimals,
260 + String? tokenMint,
261 + required double inputAmount,
262 + required String destinationAddress,
263 + required Ed25519HDKeyPair ownerKeypair,
264 + List<String> references = const [],
265 + }) async {
266 + const commitment = Commitment.finalized;
267 +
268 + final latestBlockhash =
269 + await _client!.rpcClient.getLatestBlockhash(commitment: commitment).value;
270 +
271 + final recentBlockhash = RecentBlockhash(
272 + blockhash: latestBlockhash.blockhash,
273 + feeCalculator: const FeeCalculator(
274 + lamportsPerSignature: 500,
275 + ),
276 + );
277 +
278 + if (tokenTitle == CryptoCurrency.sol.title) {
279 + final pendingNativeTokenTransaction = await _signNativeTokenTransaction(
280 + tokenTitle: tokenTitle,
281 + tokenDecimals: tokenDecimals,
282 + inputAmount: inputAmount,
283 + destinationAddress: destinationAddress,
284 + ownerKeypair: ownerKeypair,
285 + recentBlockhash: recentBlockhash,
286 + commitment: commitment,
287 + );
288 + return pendingNativeTokenTransaction;
289 + } else {
290 + final pendingSPLTokenTransaction = _signSPLTokenTransaction(
291 + tokenTitle: tokenTitle,
292 + tokenDecimals: tokenDecimals,
293 + tokenMint: tokenMint!,
294 + inputAmount: inputAmount,
295 + destinationAddress: destinationAddress,
296 + ownerKeypair: ownerKeypair,
297 + recentBlockhash: recentBlockhash,
298 + commitment: commitment,
299 + );
300 + return pendingSPLTokenTransaction;
301 + }
302 + }
303 +
304 + Future<PendingSolanaTransaction> _signNativeTokenTransaction({
305 + required String tokenTitle,
306 + required int tokenDecimals,
307 + required double inputAmount,
308 + required String destinationAddress,
309 + required Ed25519HDKeyPair ownerKeypair,
310 + required RecentBlockhash recentBlockhash,
311 + required Commitment commitment,
312 + }) async {
313 + // Convert SOL to lamport
314 + int lamports = (inputAmount * lamportsPerSol).toInt();
315 +
316 + final instructions = [
317 + SystemInstruction.transfer(
318 + fundingAccount: ownerKeypair.publicKey,
319 + recipientAccount: Ed25519HDPublicKey.fromBase58(destinationAddress),
320 + lamports: lamports,
321 + ),
322 + ];
323 +
324 + final message = Message(instructions: instructions);
325 + final signers = [ownerKeypair];
326 +
327 + final signedTx = await _signTransactionInternal(
328 + message: message,
329 + signers: signers,
330 + commitment: commitment,
331 + recentBlockhash: recentBlockhash,
332 + );
333 +
334 + final fee = await _getFeeFromCompiledMessage(
335 + message,
336 + recentBlockhash,
337 + signers.first.publicKey,
338 + );
339 +
340 + sendTx() async => await sendTransaction(
341 + signedTransaction: signedTx,
342 + commitment: commitment,
343 + );
344 +
345 + final pendingTransaction = PendingSolanaTransaction(
346 + amount: inputAmount,
347 + signedTransaction: signedTx,
348 + destinationAddress: destinationAddress,
349 + sendTransaction: sendTx,
350 + fee: fee,
351 + );
352 +
353 + return pendingTransaction;
354 + }
355 +
356 + Future<PendingSolanaTransaction> _signSPLTokenTransaction({
357 + required String tokenTitle,
358 + required int tokenDecimals,
359 + required String tokenMint,
360 + required double inputAmount,
361 + required String destinationAddress,
362 + required Ed25519HDKeyPair ownerKeypair,
363 + required RecentBlockhash recentBlockhash,
364 + required Commitment commitment,
365 + }) async {
366 + final destinationOwner = Ed25519HDPublicKey.fromBase58(destinationAddress);
367 + final mint = Ed25519HDPublicKey.fromBase58(tokenMint);
368 +
369 + ProgramAccount? associatedRecipientAccount;
370 + ProgramAccount? associatedSenderAccount;
371 +
372 + associatedRecipientAccount = await _client!.getAssociatedTokenAccount(
373 + mint: mint,
374 + owner: destinationOwner,
375 + commitment: commitment,
376 + );
377 +
378 + associatedSenderAccount = await _client!.getAssociatedTokenAccount(
379 + owner: ownerKeypair.publicKey,
380 + mint: mint,
381 + commitment: commitment,
382 + );
383 +
384 + // Throw an appropriate exception if the sender has no associated
385 + // token account
386 + if (associatedSenderAccount == null) {
387 + throw NoAssociatedTokenAccountException(ownerKeypair.address, mint.toBase58());
388 + }
389 +
390 + try {
391 + associatedRecipientAccount ??= await _client!.createAssociatedTokenAccount(
392 + mint: mint,
393 + owner: destinationOwner,
394 + funder: ownerKeypair,
395 + );
396 + } catch (e) {
397 + throw Exception(
398 + 'Error while creating an associated token account for the recipient: ${e.toString()}',
399 + );
400 + }
401 +
402 + // Input by the user
403 + final amount = (inputAmount * pow(10, tokenDecimals)).toInt();
404 +
405 + final instruction = TokenInstruction.transfer(
406 + source: Ed25519HDPublicKey.fromBase58(associatedSenderAccount.pubkey),
407 + destination: Ed25519HDPublicKey.fromBase58(associatedRecipientAccount.pubkey),
408 + owner: ownerKeypair.publicKey,
409 + amount: amount,
410 + );
411 +
412 + final message = Message(instructions: [instruction]);
413 + final signers = [ownerKeypair];
414 +
415 + final signedTx = await _signTransactionInternal(
416 + message: message,
417 + signers: signers,
418 + commitment: commitment,
419 + recentBlockhash: recentBlockhash,
420 + );
421 +
422 + final fee = await _getFeeFromCompiledMessage(
423 + message,
424 + recentBlockhash,
425 + signers.first.publicKey,
426 + );
427 +
428 + sendTx() async => await sendTransaction(
429 + signedTransaction: signedTx,
430 + commitment: commitment,
431 + );
432 +
433 + final pendingTransaction = PendingSolanaTransaction(
434 + amount: inputAmount,
435 + signedTransaction: signedTx,
436 + destinationAddress: destinationAddress,
437 + sendTransaction: sendTx,
438 + fee: fee,
439 + );
440 + return pendingTransaction;
441 + }
442 +
443 + Future<double> _getFeeFromCompiledMessage(
444 + Message message, RecentBlockhash recentBlockhash, Ed25519HDPublicKey feePayer) async {
445 + final compile = message.compile(
446 + recentBlockhash: recentBlockhash.blockhash,
447 + feePayer: feePayer,
448 + );
449 +
450 + final base64Message = base64Encode(compile.toByteArray().toList());
451 +
452 + final fee = await getGasForMessage(base64Message);
453 + return fee;
454 + }
455 +
456 + Future<SignedTx> _signTransactionInternal({
457 + required Message message,
458 + required List<Ed25519HDKeyPair> signers,
459 + required Commitment commitment,
460 + required RecentBlockhash recentBlockhash,
461 + }) async {
462 + final signedTx = await signTransaction(recentBlockhash, message, signers);
463 +
464 + return signedTx;
465 + }
466 +
467 + Future<String> sendTransaction({
468 + required SignedTx signedTransaction,
469 + required Commitment commitment,
470 + }) async {
471 + final signature = await _client!.rpcClient.sendTransaction(signedTransaction.encode());
472 +
473 + _client!.waitForSignatureStatus(signature, status: commitment);
474 +
475 + return signature;
476 + }
477 +}
cw_solana/lib/solana_exceptions.dart new
+21
@@ -0,0 +1,21 @@
1 +import 'package:cw_core/crypto_currency.dart';
2 +
3 +class SolanaTransactionCreationException implements Exception {
4 + final String exceptionMessage;
5 +
6 + SolanaTransactionCreationException(CryptoCurrency currency)
7 + : exceptionMessage = 'Error creating ${currency.title} transaction.';
8 +
9 + @override
10 + String toString() => exceptionMessage;
11 +}
12 +
13 +class SolanaTransactionWrongBalanceException implements Exception {
14 + final String exceptionMessage;
15 +
16 + SolanaTransactionWrongBalanceException(CryptoCurrency currency)
17 + : exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.';
18 +
19 + @override
20 + String toString() => exceptionMessage;
21 +}
cw_solana/lib/solana_mnemonics.dart new
+2058
@@ -0,0 +1,2058 @@
1 +class SolanaMnemonicIsIncorrectException implements Exception {
2 + @override
3 + String toString() =>
4 + 'Solana mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.';
5 +}
6 +
7 +class SolanaMnemonics {
8 + static const englishWordlist = <String>[
9 + 'abandon',
10 + 'ability',
11 + 'able',
12 + 'about',
13 + 'above',
14 + 'absent',
15 + 'absorb',
16 + 'abstract',
17 + 'absurd',
18 + 'abuse',
19 + 'access',
20 + 'accident',
21 + 'account',
22 + 'accuse',
23 + 'achieve',
24 + 'acid',
25 + 'acoustic',
26 + 'acquire',
27 + 'across',
28 + 'act',
29 + 'action',
30 + 'actor',
31 + 'actress',
32 + 'actual',
33 + 'adapt',
34 + 'add',
35 + 'addict',
36 + 'address',
37 + 'adjust',
38 + 'admit',
39 + 'adult',
40 + 'advance',
41 + 'advice',
42 + 'aerobic',
43 + 'affair',
44 + 'afford',
45 + 'afraid',
46 + 'again',
47 + 'age',
48 + 'agent',
49 + 'agree',
50 + 'ahead',
51 + 'aim',
52 + 'air',
53 + 'airport',
54 + 'aisle',
55 + 'alarm',
56 + 'album',
57 + 'alcohol',
58 + 'alert',
59 + 'alien',
60 + 'all',
61 + 'alley',
62 + 'allow',
63 + 'almost',
64 + 'alone',
65 + 'alpha',
66 + 'already',
67 + 'also',
68 + 'alter',
69 + 'always',
70 + 'amateur',
71 + 'amazing',
72 + 'among',
73 + 'amount',
74 + 'amused',
75 + 'analyst',
76 + 'anchor',
77 + 'ancient',
78 + 'anger',
79 + 'angle',
80 + 'angry',
81 + 'animal',
82 + 'ankle',
83 + 'announce',
84 + 'annual',
85 + 'another',
86 + 'answer',
87 + 'antenna',
88 + 'antique',
89 + 'anxiety',
90 + 'any',
91 + 'apart',
92 + 'apology',
93 + 'appear',
94 + 'apple',
95 + 'approve',
96 + 'april',
97 + 'arch',
98 + 'arctic',
99 + 'area',
100 + 'arena',
101 + 'argue',
102 + 'arm',
103 + 'armed',
104 + 'armor',
105 + 'army',
106 + 'around',
107 + 'arrange',
108 + 'arrest',
109 + 'arrive',
110 + 'arrow',
111 + 'art',
112 + 'artefact',
113 + 'artist',
114 + 'artwork',
115 + 'ask',
116 + 'aspect',
117 + 'assault',
118 + 'asset',
119 + 'assist',
120 + 'assume',
121 + 'asthma',
122 + 'athlete',
123 + 'atom',
124 + 'attack',
125 + 'attend',
126 + 'attitude',
127 + 'attract',
128 + 'auction',
129 + 'audit',
130 + 'august',
131 + 'aunt',
132 + 'author',
133 + 'auto',
134 + 'autumn',
135 + 'average',
136 + 'avocado',
137 + 'avoid',
138 + 'awake',
139 + 'aware',
140 + 'away',
141 + 'awesome',
142 + 'awful',
143 + 'awkward',
144 + 'axis',
145 + 'baby',
146 + 'bachelor',
147 + 'bacon',
148 + 'badge',
149 + 'bag',
150 + 'balance',
151 + 'balcony',
152 + 'ball',
153 + 'bamboo',
154 + 'banana',
155 + 'banner',
156 + 'bar',
157 + 'barely',
158 + 'bargain',
159 + 'barrel',
160 + 'base',
161 + 'basic',
162 + 'basket',
163 + 'battle',
164 + 'beach',
165 + 'bean',
166 + 'beauty',
167 + 'because',
168 + 'become',
169 + 'beef',
170 + 'before',
171 + 'begin',
172 + 'behave',
173 + 'behind',
174 + 'believe',
175 + 'below',
176 + 'belt',
177 + 'bench',
178 + 'benefit',
179 + 'best',
180 + 'betray',
181 + 'better',
182 + 'between',
183 + 'beyond',
184 + 'bicycle',
185 + 'bid',
186 + 'bike',
187 + 'bind',
188 + 'biology',
189 + 'bird',
190 + 'birth',
191 + 'bitter',
192 + 'black',
193 + 'blade',
194 + 'blame',
195 + 'blanket',
196 + 'blast',
197 + 'bleak',
198 + 'bless',
199 + 'blind',
200 + 'blood',
201 + 'blossom',
202 + 'blouse',
203 + 'blue',
204 + 'blur',
205 + 'blush',
206 + 'board',
207 + 'boat',
208 + 'body',
209 + 'boil',
210 + 'bomb',
211 + 'bone',
212 + 'bonus',
213 + 'book',
214 + 'boost',
215 + 'border',
216 + 'boring',
217 + 'borrow',
218 + 'boss',
219 + 'bottom',
220 + 'bounce',
221 + 'box',
222 + 'boy',
223 + 'bracket',
224 + 'brain',
225 + 'brand',
226 + 'brass',
227 + 'brave',
228 + 'bread',
229 + 'breeze',
230 + 'brick',
231 + 'bridge',
232 + 'brief',
233 + 'bright',
234 + 'bring',
235 + 'brisk',
236 + 'broccoli',
237 + 'broken',
238 + 'bronze',
239 + 'broom',
240 + 'brother',
241 + 'brown',
242 + 'brush',
243 + 'bubble',
244 + 'buddy',
245 + 'budget',
246 + 'buffalo',
247 + 'build',
248 + 'bulb',
249 + 'bulk',
250 + 'bullet',
251 + 'bundle',
252 + 'bunker',
253 + 'burden',
254 + 'burger',
255 + 'burst',
256 + 'bus',
257 + 'business',
258 + 'busy',
259 + 'butter',
260 + 'buyer',
261 + 'buzz',
262 + 'cabbage',
263 + 'cabin',
264 + 'cable',
265 + 'cactus',
266 + 'cage',
267 + 'cake',
268 + 'call',
269 + 'calm',
270 + 'camera',
271 + 'camp',
272 + 'can',
273 + 'canal',
274 + 'cancel',
275 + 'candy',
276 + 'cannon',
277 + 'canoe',
278 + 'canvas',
279 + 'canyon',
280 + 'capable',
281 + 'capital',
282 + 'captain',
283 + 'car',
284 + 'carbon',
285 + 'card',
286 + 'cargo',
287 + 'carpet',
288 + 'carry',
289 + 'cart',
290 + 'case',
291 + 'cash',
292 + 'casino',
293 + 'castle',
294 + 'casual',
295 + 'cat',
296 + 'catalog',
297 + 'catch',
298 + 'category',
299 + 'cattle',
300 + 'caught',
301 + 'cause',
302 + 'caution',
303 + 'cave',
304 + 'ceiling',
305 + 'celery',
306 + 'cement',
307 + 'census',
308 + 'century',
309 + 'cereal',
310 + 'certain',
311 + 'chair',
312 + 'chalk',
313 + 'champion',
314 + 'change',
315 + 'chaos',
316 + 'chapter',
317 + 'charge',
318 + 'chase',
319 + 'chat',
320 + 'cheap',
321 + 'check',
322 + 'cheese',
323 + 'chef',
324 + 'cherry',
325 + 'chest',
326 + 'chicken',
327 + 'chief',
328 + 'child',
329 + 'chimney',
330 + 'choice',
331 + 'choose',
332 + 'chronic',
333 + 'chuckle',
334 + 'chunk',
335 + 'churn',
336 + 'cigar',
337 + 'cinnamon',
338 + 'circle',
339 + 'citizen',
340 + 'city',
341 + 'civil',
342 + 'claim',
343 + 'clap',
344 + 'clarify',
345 + 'claw',
346 + 'clay',
347 + 'clean',
348 + 'clerk',
349 + 'clever',
350 + 'click',
351 + 'client',
352 + 'cliff',
353 + 'climb',
354 + 'clinic',
355 + 'clip',
356 + 'clock',
357 + 'clog',
358 + 'close',
359 + 'cloth',
360 + 'cloud',
361 + 'clown',
362 + 'club',
363 + 'clump',
364 + 'cluster',
365 + 'clutch',
366 + 'coach',
367 + 'coast',
368 + 'coconut',
369 + 'code',
370 + 'coffee',
371 + 'coil',
372 + 'coin',
373 + 'collect',
374 + 'color',
375 + 'column',
376 + 'combine',
377 + 'come',
378 + 'comfort',
379 + 'comic',
380 + 'common',
381 + 'company',
382 + 'concert',
383 + 'conduct',
384 + 'confirm',
385 + 'congress',
386 + 'connect',
387 + 'consider',
388 + 'control',
389 + 'convince',
390 + 'cook',
391 + 'cool',
392 + 'copper',
393 + 'copy',
394 + 'coral',
395 + 'core',
396 + 'corn',
397 + 'correct',
398 + 'cost',
399 + 'cotton',
400 + 'couch',
401 + 'country',
402 + 'couple',
403 + 'course',
404 + 'cousin',
405 + 'cover',
406 + 'coyote',
407 + 'crack',
408 + 'cradle',
409 + 'craft',
410 + 'cram',
411 + 'crane',
412 + 'crash',
413 + 'crater',
414 + 'crawl',
415 + 'crazy',
416 + 'cream',
417 + 'credit',
418 + 'creek',
419 + 'crew',
420 + 'cricket',
421 + 'crime',
422 + 'crisp',
423 + 'critic',
424 + 'crop',
425 + 'cross',
426 + 'crouch',
427 + 'crowd',
428 + 'crucial',
429 + 'cruel',
430 + 'cruise',
431 + 'crumble',
432 + 'crunch',
433 + 'crush',
434 + 'cry',
435 + 'crystal',
436 + 'cube',
437 + 'culture',
438 + 'cup',
439 + 'cupboard',
440 + 'curious',
441 + 'current',
442 + 'curtain',
443 + 'curve',
444 + 'cushion',
445 + 'custom',
446 + 'cute',
447 + 'cycle',
448 + 'dad',
449 + 'damage',
450 + 'damp',
451 + 'dance',
452 + 'danger',
453 + 'daring',
454 + 'dash',
455 + 'daughter',
456 + 'dawn',
457 + 'day',
458 + 'deal',
459 + 'debate',
460 + 'debris',
461 + 'decade',
462 + 'december',
463 + 'decide',
464 + 'decline',
465 + 'decorate',
466 + 'decrease',
467 + 'deer',
468 + 'defense',
469 + 'define',
470 + 'defy',
471 + 'degree',
472 + 'delay',
473 + 'deliver',
474 + 'demand',
475 + 'demise',
476 + 'denial',
477 + 'dentist',
478 + 'deny',
479 + 'depart',
480 + 'depend',
481 + 'deposit',
482 + 'depth',
483 + 'deputy',
484 + 'derive',
485 + 'describe',
486 + 'desert',
487 + 'design',
488 + 'desk',
489 + 'despair',
490 + 'destroy',
491 + 'detail',
492 + 'detect',
493 + 'develop',
494 + 'device',
495 + 'devote',
496 + 'diagram',
497 + 'dial',
498 + 'diamond',
499 + 'diary',
500 + 'dice',
501 + 'diesel',
502 + 'diet',
503 + 'differ',
504 + 'digital',
505 + 'dignity',
506 + 'dilemma',
507 + 'dinner',
508 + 'dinosaur',
509 + 'direct',
510 + 'dirt',
511 + 'disagree',
512 + 'discover',
513 + 'disease',
514 + 'dish',
515 + 'dismiss',
516 + 'disorder',
517 + 'display',
518 + 'distance',
519 + 'divert',
520 + 'divide',
521 + 'divorce',
522 + 'dizzy',
523 + 'doctor',
524 + 'document',
525 + 'dog',
526 + 'doll',
527 + 'dolphin',
528 + 'domain',
529 + 'donate',
530 + 'donkey',
531 + 'donor',
532 + 'door',
533 + 'dose',
534 + 'double',
535 + 'dove',
536 + 'draft',
537 + 'dragon',
538 + 'drama',
539 + 'drastic',
540 + 'draw',
541 + 'dream',
542 + 'dress',
543 + 'drift',
544 + 'drill',
545 + 'drink',
546 + 'drip',
547 + 'drive',
548 + 'drop',
549 + 'drum',
550 + 'dry',
551 + 'duck',
552 + 'dumb',
553 + 'dune',
554 + 'during',
555 + 'dust',
556 + 'dutch',
557 + 'duty',
558 + 'dwarf',
559 + 'dynamic',
560 + 'eager',
561 + 'eagle',
562 + 'early',
563 + 'earn',
564 + 'earth',
565 + 'easily',
566 + 'east',
567 + 'easy',
568 + 'echo',
569 + 'ecology',
570 + 'economy',
571 + 'edge',
572 + 'edit',
573 + 'educate',
574 + 'effort',
575 + 'egg',
576 + 'eight',
577 + 'either',
578 + 'elbow',
579 + 'elder',
580 + 'electric',
581 + 'elegant',
582 + 'element',
583 + 'elephant',
584 + 'elevator',
585 + 'elite',
586 + 'else',
587 + 'embark',
588 + 'embody',
589 + 'embrace',
590 + 'emerge',
591 + 'emotion',
592 + 'employ',
593 + 'empower',
594 + 'empty',
595 + 'enable',
596 + 'enact',
597 + 'end',
598 + 'endless',
599 + 'endorse',
600 + 'enemy',
601 + 'energy',
602 + 'enforce',
603 + 'engage',
604 + 'engine',
605 + 'enhance',
606 + 'enjoy',
607 + 'enlist',
608 + 'enough',
609 + 'enrich',
610 + 'enroll',
611 + 'ensure',
612 + 'enter',
613 + 'entire',
614 + 'entry',
615 + 'envelope',
616 + 'episode',
617 + 'equal',
618 + 'equip',
619 + 'era',
620 + 'erase',
621 + 'erode',
622 + 'erosion',
623 + 'error',
624 + 'erupt',
625 + 'escape',
626 + 'essay',
627 + 'essence',
628 + 'estate',
629 + 'eternal',
630 + 'ethics',
631 + 'evidence',
632 + 'evil',
633 + 'evoke',
634 + 'evolve',
635 + 'exact',
636 + 'example',
637 + 'excess',
638 + 'exchange',
639 + 'excite',
640 + 'exclude',
641 + 'excuse',
642 + 'execute',
643 + 'exercise',
644 + 'exhaust',
645 + 'exhibit',
646 + 'exile',
647 + 'exist',
648 + 'exit',
649 + 'exotic',
650 + 'expand',
651 + 'expect',
652 + 'expire',
653 + 'explain',
654 + 'expose',
655 + 'express',
656 + 'extend',
657 + 'extra',
658 + 'eye',
659 + 'eyebrow',
660 + 'fabric',
661 + 'face',
662 + 'faculty',
663 + 'fade',
664 + 'faint',
665 + 'faith',
666 + 'fall',
667 + 'false',
668 + 'fame',
669 + 'family',
670 + 'famous',
671 + 'fan',
672 + 'fancy',
673 + 'fantasy',
674 + 'farm',
675 + 'fashion',
676 + 'fat',
677 + 'fatal',
678 + 'father',
679 + 'fatigue',
680 + 'fault',
681 + 'favorite',
682 + 'feature',
683 + 'february',
684 + 'federal',
685 + 'fee',
686 + 'feed',
687 + 'feel',
688 + 'female',
689 + 'fence',
690 + 'festival',
691 + 'fetch',
692 + 'fever',
693 + 'few',
694 + 'fiber',
695 + 'fiction',
696 + 'field',
697 + 'figure',
698 + 'file',
699 + 'film',
700 + 'filter',
701 + 'final',
702 + 'find',
703 + 'fine',
704 + 'finger',
705 + 'finish',
706 + 'fire',
707 + 'firm',
708 + 'first',
709 + 'fiscal',
710 + 'fish',
711 + 'fit',
712 + 'fitness',
713 + 'fix',
714 + 'flag',
715 + 'flame',
716 + 'flash',
717 + 'flat',
718 + 'flavor',
719 + 'flee',
720 + 'flight',
721 + 'flip',
722 + 'float',
723 + 'flock',
724 + 'floor',
725 + 'flower',
726 + 'fluid',
727 + 'flush',
728 + 'fly',
729 + 'foam',
730 + 'focus',
731 + 'fog',
732 + 'foil',
733 + 'fold',
734 + 'follow',
735 + 'food',
736 + 'foot',
737 + 'force',
738 + 'forest',
739 + 'forget',
740 + 'fork',
741 + 'fortune',
742 + 'forum',
743 + 'forward',
744 + 'fossil',
745 + 'foster',
746 + 'found',
747 + 'fox',
748 + 'fragile',
749 + 'frame',
750 + 'frequent',
751 + 'fresh',
752 + 'friend',
753 + 'fringe',
754 + 'frog',
755 + 'front',
756 + 'frost',
757 + 'frown',
758 + 'frozen',
759 + 'fruit',
760 + 'fuel',
761 + 'fun',
762 + 'funny',
763 + 'furnace',
764 + 'fury',
765 + 'future',
766 + 'gadget',
767 + 'gain',
768 + 'galaxy',
769 + 'gallery',
770 + 'game',
771 + 'gap',
772 + 'garage',
773 + 'garbage',
774 + 'garden',
775 + 'garlic',
776 + 'garment',
777 + 'gas',
778 + 'gasp',
779 + 'gate',
780 + 'gather',
781 + 'gauge',
782 + 'gaze',
783 + 'general',
784 + 'genius',
785 + 'genre',
786 + 'gentle',
787 + 'genuine',
788 + 'gesture',
789 + 'ghost',
790 + 'giant',
791 + 'gift',
792 + 'giggle',
793 + 'ginger',
794 + 'giraffe',
795 + 'girl',
796 + 'give',
797 + 'glad',
798 + 'glance',
799 + 'glare',
800 + 'glass',
801 + 'glide',
802 + 'glimpse',
803 + 'globe',
804 + 'gloom',
805 + 'glory',
806 + 'glove',
807 + 'glow',
808 + 'glue',
809 + 'goat',
810 + 'goddess',
811 + 'gold',
812 + 'good',
813 + 'goose',
814 + 'gorilla',
815 + 'gospel',
816 + 'gossip',
817 + 'govern',
818 + 'gown',
819 + 'grab',
820 + 'grace',
821 + 'grain',
822 + 'grant',
823 + 'grape',
824 + 'grass',
825 + 'gravity',
826 + 'great',
827 + 'green',
828 + 'grid',
829 + 'grief',
830 + 'grit',
831 + 'grocery',
832 + 'group',
833 + 'grow',
834 + 'grunt',
835 + 'guard',
836 + 'guess',
837 + 'guide',
838 + 'guilt',
839 + 'guitar',
840 + 'gun',
841 + 'gym',
842 + 'habit',
843 + 'hair',
844 + 'half',
845 + 'hammer',
846 + 'hamster',
847 + 'hand',
848 + 'happy',
849 + 'harbor',
850 + 'hard',
851 + 'harsh',
852 + 'harvest',
853 + 'hat',
854 + 'have',
855 + 'hawk',
856 + 'hazard',
857 + 'head',
858 + 'health',
859 + 'heart',
860 + 'heavy',
861 + 'hedgehog',
862 + 'height',
863 + 'hello',
864 + 'helmet',
865 + 'help',
866 + 'hen',
867 + 'hero',
868 + 'hidden',
869 + 'high',
870 + 'hill',
871 + 'hint',
872 + 'hip',
873 + 'hire',
874 + 'history',
875 + 'hobby',
876 + 'hockey',
877 + 'hold',
878 + 'hole',
879 + 'holiday',
880 + 'hollow',
881 + 'home',
882 + 'honey',
883 + 'hood',
884 + 'hope',
885 + 'horn',
886 + 'horror',
887 + 'horse',
888 + 'hospital',
889 + 'host',
890 + 'hotel',
891 + 'hour',
892 + 'hover',
893 + 'hub',
894 + 'huge',
895 + 'human',
896 + 'humble',
897 + 'humor',
898 + 'hundred',
899 + 'hungry',
900 + 'hunt',
901 + 'hurdle',
902 + 'hurry',
903 + 'hurt',
904 + 'husband',
905 + 'hybrid',
906 + 'ice',
907 + 'icon',
908 + 'idea',
909 + 'identify',
910 + 'idle',
911 + 'ignore',
912 + 'ill',
913 + 'illegal',
914 + 'illness',
915 + 'image',
916 + 'imitate',
917 + 'immense',
918 + 'immune',
919 + 'impact',
920 + 'impose',
921 + 'improve',
922 + 'impulse',
923 + 'inch',
924 + 'include',
925 + 'income',
926 + 'increase',
927 + 'index',
928 + 'indicate',
929 + 'indoor',
930 + 'industry',
931 + 'infant',
932 + 'inflict',
933 + 'inform',
934 + 'inhale',
935 + 'inherit',
936 + 'initial',
937 + 'inject',
938 + 'injury',
939 + 'inmate',
940 + 'inner',
941 + 'innocent',
942 + 'input',
943 + 'inquiry',
944 + 'insane',
945 + 'insect',
946 + 'inside',
947 + 'inspire',
948 + 'install',
949 + 'intact',
950 + 'interest',
951 + 'into',
952 + 'invest',
953 + 'invite',
954 + 'involve',
955 + 'iron',
956 + 'island',
957 + 'isolate',
958 + 'issue',
959 + 'item',
960 + 'ivory',
961 + 'jacket',
962 + 'jaguar',
963 + 'jar',
964 + 'jazz',
965 + 'jealous',
966 + 'jeans',
967 + 'jelly',
968 + 'jewel',
969 + 'job',
970 + 'join',
971 + 'joke',
972 + 'journey',
973 + 'joy',
974 + 'judge',
975 + 'juice',
976 + 'jump',
977 + 'jungle',
978 + 'junior',
979 + 'junk',
980 + 'just',
981 + 'kangaroo',
982 + 'keen',
983 + 'keep',
984 + 'ketchup',
985 + 'key',
986 + 'kick',
987 + 'kid',
988 + 'kidney',
989 + 'kind',
990 + 'kingdom',
991 + 'kiss',
992 + 'kit',
993 + 'kitchen',
994 + 'kite',
995 + 'kitten',
996 + 'kiwi',
997 + 'knee',
998 + 'knife',
999 + 'knock',
1000 + 'know',
1001 + 'lab',
1002 + 'label',
1003 + 'labor',
1004 + 'ladder',
1005 + 'lady',
1006 + 'lake',
1007 + 'lamp',
1008 + 'language',
1009 + 'laptop',
1010 + 'large',
1011 + 'later',
1012 + 'latin',
1013 + 'laugh',
1014 + 'laundry',
1015 + 'lava',
1016 + 'law',
1017 + 'lawn',
1018 + 'lawsuit',
1019 + 'layer',
1020 + 'lazy',
1021 + 'leader',
1022 + 'leaf',
1023 + 'learn',
1024 + 'leave',
1025 + 'lecture',
1026 + 'left',
1027 + 'leg',
1028 + 'legal',
1029 + 'legend',
1030 + 'leisure',
1031 + 'lemon',
1032 + 'lend',
1033 + 'length',
1034 + 'lens',
1035 + 'leopard',
1036 + 'lesson',
1037 + 'letter',
1038 + 'level',
1039 + 'liar',
1040 + 'liberty',
1041 + 'library',
1042 + 'license',
1043 + 'life',
1044 + 'lift',
1045 + 'light',
1046 + 'like',
1047 + 'limb',
1048 + 'limit',
1049 + 'link',
1050 + 'lion',
1051 + 'liquid',
1052 + 'list',
1053 + 'little',
1054 + 'live',
1055 + 'lizard',
1056 + 'load',
1057 + 'loan',
1058 + 'lobster',
1059 + 'local',
1060 + 'lock',
1061 + 'logic',
1062 + 'lonely',
1063 + 'long',
1064 + 'loop',
1065 + 'lottery',
1066 + 'loud',
1067 + 'lounge',
1068 + 'love',
1069 + 'loyal',
1070 + 'lucky',
1071 + 'luggage',
1072 + 'lumber',
1073 + 'lunar',
1074 + 'lunch',
1075 + 'luxury',
1076 + 'lyrics',
1077 + 'machine',
1078 + 'mad',
1079 + 'magic',
1080 + 'magnet',
1081 + 'maid',
1082 + 'mail',
1083 + 'main',
1084 + 'major',
1085 + 'make',
1086 + 'mammal',
1087 + 'man',
1088 + 'manage',
1089 + 'mandate',
1090 + 'mango',
1091 + 'mansion',
1092 + 'manual',
1093 + 'maple',
1094 + 'marble',
1095 + 'march',
1096 + 'margin',
1097 + 'marine',
1098 + 'market',
1099 + 'marriage',
1100 + 'mask',
1101 + 'mass',
1102 + 'master',
1103 + 'match',
1104 + 'material',
1105 + 'math',
1106 + 'matrix',
1107 + 'matter',
1108 + 'maximum',
1109 + 'maze',
1110 + 'meadow',
1111 + 'mean',
1112 + 'measure',
1113 + 'meat',
1114 + 'mechanic',
1115 + 'medal',
1116 + 'media',
1117 + 'melody',
1118 + 'melt',
1119 + 'member',
1120 + 'memory',
1121 + 'mention',
1122 + 'menu',
1123 + 'mercy',
1124 + 'merge',
1125 + 'merit',
1126 + 'merry',
1127 + 'mesh',
1128 + 'message',
1129 + 'metal',
1130 + 'method',
1131 + 'middle',
1132 + 'midnight',
1133 + 'milk',
1134 + 'million',
1135 + 'mimic',
1136 + 'mind',
1137 + 'minimum',
1138 + 'minor',
1139 + 'minute',
1140 + 'miracle',
1141 + 'mirror',
1142 + 'misery',
1143 + 'miss',
1144 + 'mistake',
1145 + 'mix',
1146 + 'mixed',
1147 + 'mixture',
1148 + 'mobile',
1149 + 'model',
1150 + 'modify',
1151 + 'mom',
1152 + 'moment',
1153 + 'monitor',
1154 + 'monkey',
1155 + 'monster',
1156 + 'month',
1157 + 'moon',
1158 + 'moral',
1159 + 'more',
1160 + 'morning',
1161 + 'mosquito',
1162 + 'mother',
1163 + 'motion',
1164 + 'motor',
1165 + 'mountain',
1166 + 'mouse',
1167 + 'move',
1168 + 'movie',
1169 + 'much',
1170 + 'muffin',
1171 + 'mule',
1172 + 'multiply',
1173 + 'muscle',
1174 + 'museum',
1175 + 'mushroom',
1176 + 'music',
1177 + 'must',
1178 + 'mutual',
1179 + 'myself',
1180 + 'mystery',
1181 + 'myth',
1182 + 'naive',
1183 + 'name',
1184 + 'napkin',
1185 + 'narrow',
1186 + 'nasty',
1187 + 'nation',
1188 + 'nature',
1189 + 'near',
1190 + 'neck',
1191 + 'need',
1192 + 'negative',
1193 + 'neglect',
1194 + 'neither',
1195 + 'nephew',
1196 + 'nerve',
1197 + 'nest',
1198 + 'net',
1199 + 'network',
1200 + 'neutral',
1201 + 'never',
1202 + 'news',
1203 + 'next',
1204 + 'nice',
1205 + 'night',
1206 + 'noble',
1207 + 'noise',
1208 + 'nominee',
1209 + 'noodle',
1210 + 'normal',
1211 + 'north',
1212 + 'nose',
1213 + 'notable',
1214 + 'note',
1215 + 'nothing',
1216 + 'notice',
1217 + 'novel',
1218 + 'now',
1219 + 'nuclear',
1220 + 'number',
1221 + 'nurse',
1222 + 'nut',
1223 + 'oak',
1224 + 'obey',
1225 + 'object',
1226 + 'oblige',
1227 + 'obscure',
1228 + 'observe',
1229 + 'obtain',
1230 + 'obvious',
1231 + 'occur',
1232 + 'ocean',
1233 + 'october',
1234 + 'odor',
1235 + 'off',
1236 + 'offer',
1237 + 'office',
1238 + 'often',
1239 + 'oil',
1240 + 'okay',
1241 + 'old',
1242 + 'olive',
1243 + 'olympic',
1244 + 'omit',
1245 + 'once',
1246 + 'one',
1247 + 'onion',
1248 + 'online',
1249 + 'only',
1250 + 'open',
1251 + 'opera',
1252 + 'opinion',
1253 + 'oppose',
1254 + 'option',
1255 + 'orange',
1256 + 'orbit',
1257 + 'orchard',
1258 + 'order',
1259 + 'ordinary',
1260 + 'organ',
1261 + 'orient',
1262 + 'original',
1263 + 'orphan',
1264 + 'ostrich',
1265 + 'other',
1266 + 'outdoor',
1267 + 'outer',
1268 + 'output',
1269 + 'outside',
1270 + 'oval',
1271 + 'oven',
1272 + 'over',
1273 + 'own',
1274 + 'owner',
1275 + 'oxygen',
1276 + 'oyster',
1277 + 'ozone',
1278 + 'pact',
1279 + 'paddle',
1280 + 'page',
1281 + 'pair',
1282 + 'palace',
1283 + 'palm',
1284 + 'panda',
1285 + 'panel',
1286 + 'panic',
1287 + 'panther',
1288 + 'paper',
1289 + 'parade',
1290 + 'parent',
1291 + 'park',
1292 + 'parrot',
1293 + 'party',
1294 + 'pass',
1295 + 'patch',
1296 + 'path',
1297 + 'patient',
1298 + 'patrol',
1299 + 'pattern',
1300 + 'pause',
1301 + 'pave',
1302 + 'payment',
1303 + 'peace',
1304 + 'peanut',
1305 + 'pear',
1306 + 'peasant',
1307 + 'pelican',
1308 + 'pen',
1309 + 'penalty',
1310 + 'pencil',
1311 + 'people',
1312 + 'pepper',
1313 + 'perfect',
1314 + 'permit',
1315 + 'person',
1316 + 'pet',
1317 + 'phone',
1318 + 'photo',
1319 + 'phrase',
1320 + 'physical',
1321 + 'piano',
1322 + 'picnic',
1323 + 'picture',
1324 + 'piece',
1325 + 'pig',
1326 + 'pigeon',
1327 + 'pill',
1328 + 'pilot',
1329 + 'pink',
1330 + 'pioneer',
1331 + 'pipe',
1332 + 'pistol',
1333 + 'pitch',
1334 + 'pizza',
1335 + 'place',
1336 + 'planet',
1337 + 'plastic',
1338 + 'plate',
1339 + 'play',
1340 + 'please',
1341 + 'pledge',
1342 + 'pluck',
1343 + 'plug',
1344 + 'plunge',
1345 + 'poem',
1346 + 'poet',
1347 + 'point',
1348 + 'polar',
1349 + 'pole',
1350 + 'police',
1351 + 'pond',
1352 + 'pony',
1353 + 'pool',
1354 + 'popular',
1355 + 'portion',
1356 + 'position',
1357 + 'possible',
1358 + 'post',
1359 + 'potato',
1360 + 'pottery',
1361 + 'poverty',
1362 + 'powder',
1363 + 'power',
1364 + 'practice',
1365 + 'praise',
1366 + 'predict',
1367 + 'prefer',
1368 + 'prepare',
1369 + 'present',
1370 + 'pretty',
1371 + 'prevent',
1372 + 'price',
1373 + 'pride',
1374 + 'primary',
1375 + 'print',
1376 + 'priority',
1377 + 'prison',
1378 + 'private',
1379 + 'prize',
1380 + 'problem',
1381 + 'process',
1382 + 'produce',
1383 + 'profit',
1384 + 'program',
1385 + 'project',
1386 + 'promote',
1387 + 'proof',
1388 + 'property',
1389 + 'prosper',
1390 + 'protect',
1391 + 'proud',
1392 + 'provide',
1393 + 'public',
1394 + 'pudding',
1395 + 'pull',
1396 + 'pulp',
1397 + 'pulse',
1398 + 'pumpkin',
1399 + 'punch',
1400 + 'pupil',
1401 + 'puppy',
1402 + 'purchase',
1403 + 'purity',
1404 + 'purpose',
1405 + 'purse',
1406 + 'push',
1407 + 'put',
1408 + 'puzzle',
1409 + 'pyramid',
1410 + 'quality',
1411 + 'quantum',
1412 + 'quarter',
1413 + 'question',
1414 + 'quick',
1415 + 'quit',
1416 + 'quiz',
1417 + 'quote',
1418 + 'rabbit',
1419 + 'raccoon',
1420 + 'race',
1421 + 'rack',
1422 + 'radar',
1423 + 'radio',
1424 + 'rail',
1425 + 'rain',
1426 + 'raise',
1427 + 'rally',
1428 + 'ramp',
1429 + 'ranch',
1430 + 'random',
1431 + 'range',
1432 + 'rapid',
1433 + 'rare',
1434 + 'rate',
1435 + 'rather',
1436 + 'raven',
1437 + 'raw',
1438 + 'razor',
1439 + 'ready',
1440 + 'real',
1441 + 'reason',
1442 + 'rebel',
1443 + 'rebuild',
1444 + 'recall',
1445 + 'receive',
1446 + 'recipe',
1447 + 'record',
1448 + 'recycle',
1449 + 'reduce',
1450 + 'reflect',
1451 + 'reform',
1452 + 'refuse',
1453 + 'region',
1454 + 'regret',
1455 + 'regular',
1456 + 'reject',
1457 + 'relax',
1458 + 'release',
1459 + 'relief',
1460 + 'rely',
1461 + 'remain',
1462 + 'remember',
1463 + 'remind',
1464 + 'remove',
1465 + 'render',
1466 + 'renew',
1467 + 'rent',
1468 + 'reopen',
1469 + 'repair',
1470 + 'repeat',
1471 + 'replace',
1472 + 'report',
1473 + 'require',
1474 + 'rescue',
1475 + 'resemble',
1476 + 'resist',
1477 + 'resource',
1478 + 'response',
1479 + 'result',
1480 + 'retire',
1481 + 'retreat',
1482 + 'return',
1483 + 'reunion',
1484 + 'reveal',
1485 + 'review',
1486 + 'reward',
1487 + 'rhythm',
1488 + 'rib',
1489 + 'ribbon',
1490 + 'rice',
1491 + 'rich',
1492 + 'ride',
1493 + 'ridge',
1494 + 'rifle',
1495 + 'right',
1496 + 'rigid',
1497 + 'ring',
1498 + 'riot',
1499 + 'ripple',
1500 + 'risk',
1501 + 'ritual',
1502 + 'rival',
1503 + 'river',
1504 + 'road',
1505 + 'roast',
1506 + 'robot',
1507 + 'robust',
1508 + 'rocket',
1509 + 'romance',
1510 + 'roof',
1511 + 'rookie',
1512 + 'room',
1513 + 'rose',
1514 + 'rotate',
1515 + 'rough',
1516 + 'round',
1517 + 'route',
1518 + 'royal',
1519 + 'rubber',
1520 + 'rude',
1521 + 'rug',
1522 + 'rule',
1523 + 'run',
1524 + 'runway',
1525 + 'rural',
1526 + 'sad',
1527 + 'saddle',
1528 + 'sadness',
1529 + 'safe',
1530 + 'sail',
1531 + 'salad',
1532 + 'salmon',
1533 + 'salon',
1534 + 'salt',
1535 + 'salute',
1536 + 'same',
1537 + 'sample',
1538 + 'sand',
1539 + 'satisfy',
1540 + 'satoshi',
1541 + 'sauce',
1542 + 'sausage',
1543 + 'save',
1544 + 'say',
1545 + 'scale',
1546 + 'scan',
1547 + 'scare',
1548 + 'scatter',
1549 + 'scene',
1550 + 'scheme',
1551 + 'school',
1552 + 'science',
1553 + 'scissors',
1554 + 'scorpion',
1555 + 'scout',
1556 + 'scrap',
1557 + 'screen',
1558 + 'script',
1559 + 'scrub',
1560 + 'sea',
1561 + 'search',
1562 + 'season',
1563 + 'seat',
1564 + 'second',
1565 + 'secret',
1566 + 'section',
1567 + 'security',
1568 + 'seed',
1569 + 'seek',
1570 + 'segment',
1571 + 'select',
1572 + 'sell',
1573 + 'seminar',
1574 + 'senior',
1575 + 'sense',
1576 + 'sentence',
1577 + 'series',
1578 + 'service',
1579 + 'session',
1580 + 'settle',
1581 + 'setup',
1582 + 'seven',
1583 + 'shadow',
1584 + 'shaft',
1585 + 'shallow',
1586 + 'share',
1587 + 'shed',
1588 + 'shell',
1589 + 'sheriff',
1590 + 'shield',
1591 + 'shift',
1592 + 'shine',
1593 + 'ship',
1594 + 'shiver',
1595 + 'shock',
1596 + 'shoe',
1597 + 'shoot',
1598 + 'shop',
1599 + 'short',
1600 + 'shoulder',
1601 + 'shove',
1602 + 'shrimp',
1603 + 'shrug',
1604 + 'shuffle',
1605 + 'shy',
1606 + 'sibling',
1607 + 'sick',
1608 + 'side',
1609 + 'siege',
1610 + 'sight',
1611 + 'sign',
1612 + 'silent',
1613 + 'silk',
1614 + 'silly',
1615 + 'silver',
1616 + 'similar',
1617 + 'simple',
1618 + 'since',
1619 + 'sing',
1620 + 'siren',
1621 + 'sister',
1622 + 'situate',
1623 + 'six',
1624 + 'size',
1625 + 'skate',
1626 + 'sketch',
1627 + 'ski',
1628 + 'skill',
1629 + 'skin',
1630 + 'skirt',
1631 + 'skull',
1632 + 'slab',
1633 + 'slam',
1634 + 'sleep',
1635 + 'slender',
1636 + 'slice',
1637 + 'slide',
1638 + 'slight',
1639 + 'slim',
1640 + 'slogan',
1641 + 'slot',
1642 + 'slow',
1643 + 'slush',
1644 + 'small',
1645 + 'smart',
1646 + 'smile',
1647 + 'smoke',
1648 + 'smooth',
1649 + 'snack',
1650 + 'snake',
1651 + 'snap',
1652 + 'sniff',
1653 + 'snow',
1654 + 'soap',
1655 + 'soccer',
1656 + 'social',
1657 + 'sock',
1658 + 'soda',
1659 + 'soft',
1660 + 'solar',
1661 + 'soldier',
1662 + 'solid',
1663 + 'solution',
1664 + 'solve',
1665 + 'someone',
1666 + 'song',
1667 + 'soon',
1668 + 'sorry',
1669 + 'sort',
1670 + 'soul',
1671 + 'sound',
1672 + 'soup',
1673 + 'source',
1674 + 'south',
1675 + 'space',
1676 + 'spare',
1677 + 'spatial',
1678 + 'spawn',
1679 + 'speak',
1680 + 'special',
1681 + 'speed',
1682 + 'spell',
1683 + 'spend',
1684 + 'sphere',
1685 + 'spice',
1686 + 'spider',
1687 + 'spike',
1688 + 'spin',
1689 + 'spirit',
1690 + 'split',
1691 + 'spoil',
1692 + 'sponsor',
1693 + 'spoon',
1694 + 'sport',
1695 + 'spot',
1696 + 'spray',
1697 + 'spread',
1698 + 'spring',
1699 + 'spy',
1700 + 'square',
1701 + 'squeeze',
1702 + 'squirrel',
1703 + 'stable',
1704 + 'stadium',
1705 + 'staff',
1706 + 'stage',
1707 + 'stairs',
1708 + 'stamp',
1709 + 'stand',
1710 + 'start',
1711 + 'state',
1712 + 'stay',
1713 + 'steak',
1714 + 'steel',
1715 + 'stem',
1716 + 'step',
1717 + 'stereo',
1718 + 'stick',
1719 + 'still',
1720 + 'sting',
1721 + 'stock',
1722 + 'stomach',
1723 + 'stone',
1724 + 'stool',
1725 + 'story',
1726 + 'stove',
1727 + 'strategy',
1728 + 'street',
1729 + 'strike',
1730 + 'strong',
1731 + 'struggle',
1732 + 'student',
1733 + 'stuff',
1734 + 'stumble',
1735 + 'style',
1736 + 'subject',
1737 + 'submit',
1738 + 'subway',
1739 + 'success',
1740 + 'such',
1741 + 'sudden',
1742 + 'suffer',
1743 + 'sugar',
1744 + 'suggest',
1745 + 'suit',
1746 + 'summer',
1747 + 'sun',
1748 + 'sunny',
1749 + 'sunset',
1750 + 'super',
1751 + 'supply',
1752 + 'supreme',
1753 + 'sure',
1754 + 'surface',
1755 + 'surge',
1756 + 'surprise',
1757 + 'surround',
1758 + 'survey',
1759 + 'suspect',
1760 + 'sustain',
1761 + 'swallow',
1762 + 'swamp',
1763 + 'swap',
1764 + 'swarm',
1765 + 'swear',
1766 + 'sweet',
1767 + 'swift',
1768 + 'swim',
1769 + 'swing',
1770 + 'switch',
1771 + 'sword',
1772 + 'symbol',
1773 + 'symptom',
1774 + 'syrup',
1775 + 'system',
1776 + 'table',
1777 + 'tackle',
1778 + 'tag',
1779 + 'tail',
1780 + 'talent',
1781 + 'talk',
1782 + 'tank',
1783 + 'tape',
1784 + 'target',
1785 + 'task',
1786 + 'taste',
1787 + 'tattoo',
1788 + 'taxi',
1789 + 'teach',
1790 + 'team',
1791 + 'tell',
1792 + 'ten',
1793 + 'tenant',
1794 + 'tennis',
1795 + 'tent',
1796 + 'term',
1797 + 'test',
1798 + 'text',
1799 + 'thank',
1800 + 'that',
1801 + 'theme',
1802 + 'then',
1803 + 'theory',
1804 + 'there',
1805 + 'they',
1806 + 'thing',
1807 + 'this',
1808 + 'thought',
1809 + 'three',
1810 + 'thrive',
1811 + 'throw',
1812 + 'thumb',
1813 + 'thunder',
1814 + 'ticket',
1815 + 'tide',
1816 + 'tiger',
1817 + 'tilt',
1818 + 'timber',
1819 + 'time',
1820 + 'tiny',
1821 + 'tip',
1822 + 'tired',
1823 + 'tissue',
1824 + 'title',
1825 + 'toast',
1826 + 'tobacco',
1827 + 'today',
1828 + 'toddler',
1829 + 'toe',
1830 + 'together',
1831 + 'toilet',
1832 + 'token',
1833 + 'tomato',
1834 + 'tomorrow',
1835 + 'tone',
1836 + 'tongue',
1837 + 'tonight',
1838 + 'tool',
1839 + 'tooth',
1840 + 'top',
1841 + 'topic',
1842 + 'topple',
1843 + 'torch',
1844 + 'tornado',
1845 + 'tortoise',
1846 + 'toss',
1847 + 'total',
1848 + 'tourist',
1849 + 'toward',
1850 + 'tower',
1851 + 'town',
1852 + 'toy',
1853 + 'track',
1854 + 'trade',
1855 + 'traffic',
1856 + 'tragic',
1857 + 'train',
1858 + 'transfer',
1859 + 'trap',
1860 + 'trash',
1861 + 'travel',
1862 + 'tray',
1863 + 'treat',
1864 + 'tree',
1865 + 'trend',
1866 + 'trial',
1867 + 'tribe',
1868 + 'trick',
1869 + 'trigger',
1870 + 'trim',
1871 + 'trip',
1872 + 'trophy',
1873 + 'trouble',
1874 + 'truck',
1875 + 'true',
1876 + 'truly',
1877 + 'trumpet',
1878 + 'trust',
1879 + 'truth',
1880 + 'try',
1881 + 'tube',
1882 + 'tuition',
1883 + 'tumble',
1884 + 'tuna',
1885 + 'tunnel',
1886 + 'turkey',
1887 + 'turn',
1888 + 'turtle',
1889 + 'twelve',
1890 + 'twenty',
1891 + 'twice',
1892 + 'twin',
1893 + 'twist',
1894 + 'two',
1895 + 'type',
1896 + 'typical',
1897 + 'ugly',
1898 + 'umbrella',
1899 + 'unable',
1900 + 'unaware',
1901 + 'uncle',
1902 + 'uncover',
1903 + 'under',
1904 + 'undo',
1905 + 'unfair',
1906 + 'unfold',
1907 + 'unhappy',
1908 + 'uniform',
1909 + 'unique',
1910 + 'unit',
1911 + 'universe',
1912 + 'unknown',
1913 + 'unlock',
1914 + 'until',
1915 + 'unusual',
1916 + 'unveil',
1917 + 'update',
1918 + 'upgrade',
1919 + 'uphold',
1920 + 'upon',
1921 + 'upper',
1922 + 'upset',
1923 + 'urban',
1924 + 'urge',
1925 + 'usage',
1926 + 'use',
1927 + 'used',
1928 + 'useful',
1929 + 'useless',
1930 + 'usual',
1931 + 'utility',
1932 + 'vacant',
1933 + 'vacuum',
1934 + 'vague',
1935 + 'valid',
1936 + 'valley',
1937 + 'valve',
1938 + 'van',
1939 + 'vanish',
1940 + 'vapor',
1941 + 'various',
1942 + 'vast',
1943 + 'vault',
1944 + 'vehicle',
1945 + 'velvet',
1946 + 'vendor',
1947 + 'venture',
1948 + 'venue',
1949 + 'verb',
1950 + 'verify',
1951 + 'version',
1952 + 'very',
1953 + 'vessel',
1954 + 'veteran',
1955 + 'viable',
1956 + 'vibrant',
1957 + 'vicious',
1958 + 'victory',
1959 + 'video',
1960 + 'view',
1961 + 'village',
1962 + 'vintage',
1963 + 'violin',
1964 + 'virtual',
1965 + 'virus',
1966 + 'visa',
1967 + 'visit',
1968 + 'visual',
1969 + 'vital',
1970 + 'vivid',
1971 + 'vocal',
1972 + 'voice',
1973 + 'void',
1974 + 'volcano',
1975 + 'volume',
1976 + 'vote',
1977 + 'voyage',
1978 + 'wage',
1979 + 'wagon',
1980 + 'wait',
1981 + 'walk',
1982 + 'wall',
1983 + 'walnut',
1984 + 'want',
1985 + 'warfare',
1986 + 'warm',
1987 + 'warrior',
1988 + 'wash',
1989 + 'wasp',
1990 + 'waste',
1991 + 'water',
1992 + 'wave',
1993 + 'way',
1994 + 'wealth',
1995 + 'weapon',
1996 + 'wear',
1997 + 'weasel',
1998 + 'weather',
1999 + 'web',
2000 + 'wedding',
2001 + 'weekend',
2002 + 'weird',
2003 + 'welcome',
2004 + 'west',
2005 + 'wet',
2006 + 'whale',
2007 + 'what',
2008 + 'wheat',
2009 + 'wheel',
2010 + 'when',
2011 + 'where',
2012 + 'whip',
2013 + 'whisper',
2014 + 'wide',
2015 + 'width',
2016 + 'wife',
2017 + 'wild',
2018 + 'will',
2019 + 'win',
2020 + 'window',
2021 + 'wine',
2022 + 'wing',
2023 + 'wink',
2024 + 'winner',
2025 + 'winter',
2026 + 'wire',
2027 + 'wisdom',
2028 + 'wise',
2029 + 'wish',
2030 + 'witness',
2031 + 'wolf',
2032 + 'woman',
2033 + 'wonder',
2034 + 'wood',
2035 + 'wool',
2036 + 'word',
2037 + 'work',
2038 + 'world',
2039 + 'worry',
2040 + 'worth',
2041 + 'wrap',
2042 + 'wreck',
2043 + 'wrestle',
2044 + 'wrist',
2045 + 'write',
2046 + 'wrong',
2047 + 'yard',
2048 + 'year',
2049 + 'yellow',
2050 + 'you',
2051 + 'young',
2052 + 'youth',
2053 + 'zebra',
2054 + 'zero',
2055 + 'zone',
2056 + 'zoo'
2057 + ];
2058 +}
cw_solana/lib/solana_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 SolanaTransactionCredentials {
5 + SolanaTransactionCredentials(
6 + this.outputs, {
7 + required this.currency,
8 + });
9 +
10 + final List<OutputInfo> outputs;
11 + final CryptoCurrency currency;
12 +}
cw_solana/lib/solana_transaction_history.dart new
+78
@@ -0,0 +1,78 @@
1 +import 'dart:convert';
2 +import 'dart:core';
3 +import 'package:cw_core/pathForWallet.dart';
4 +import 'package:cw_core/wallet_info.dart';
5 +import 'package:cw_solana/file.dart';
6 +import 'package:cw_solana/solana_transaction_info.dart';
7 +import 'package:mobx/mobx.dart';
8 +import 'package:cw_core/transaction_history.dart';
9 +
10 +part 'solana_transaction_history.g.dart';
11 +
12 +const transactionsHistoryFileName = 'solana_transactions.json';
13 +
14 +class SolanaTransactionHistory = SolanaTransactionHistoryBase with _$SolanaTransactionHistory;
15 +
16 +abstract class SolanaTransactionHistoryBase extends TransactionHistoryBase<SolanaTransactionInfo>
17 + with Store {
18 + SolanaTransactionHistoryBase({required this.walletInfo, required String password})
19 + : _password = password {
20 + transactions = ObservableMap<String, SolanaTransactionInfo>();
21 + }
22 +
23 + final WalletInfo walletInfo;
24 + String _password;
25 +
26 + Future<void> init() async => await _load();
27 +
28 + @override
29 + Future<void> save() async {
30 + try {
31 + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
32 + final path = '$dirPath/$transactionsHistoryFileName';
33 + final transactionMaps = transactions.map((key, value) => MapEntry(key, value.toJson()));
34 + final data = json.encode({'transactions': transactionMaps});
35 + await writeData(path: path, password: _password, data: data);
36 + } catch (e, s) {
37 + print('Error while saving solana transaction history: ${e.toString()}');
38 + print(s);
39 + }
40 + }
41 +
42 + @override
43 + void addOne(SolanaTransactionInfo transaction) => transactions[transaction.id] = transaction;
44 +
45 + @override
46 + void addMany(Map<String, SolanaTransactionInfo> transactions) =>
47 + this.transactions.addAll(transactions);
48 +
49 + Future<Map<String, dynamic>> _read() async {
50 + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
51 + final path = '$dirPath/$transactionsHistoryFileName';
52 + final content = await read(path: path, password: _password);
53 + if (content.isEmpty) {
54 + return {};
55 + }
56 + return json.decode(content) as Map<String, dynamic>;
57 + }
58 +
59 + Future<void> _load() async {
60 + try {
61 + final content = await _read();
62 + final txs = content['transactions'] as Map<String, dynamic>? ?? {};
63 +
64 + txs.entries.forEach((entry) {
65 + final val = entry.value;
66 +
67 + if (val is Map<String, dynamic>) {
68 + final tx = SolanaTransactionInfo.fromJson(val);
69 + _update(tx);
70 + }
71 + });
72 + } catch (e) {
73 + print(e);
74 + }
75 + }
76 +
77 + void _update(SolanaTransactionInfo transaction) => transactions[transaction.id] = transaction;
78 +}
cw_solana/lib/solana_transaction_info.dart new
+78
@@ -0,0 +1,78 @@
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 +
5 +class SolanaTransactionInfo extends TransactionInfo {
6 + SolanaTransactionInfo({
7 + required this.id,
8 + required this.blockTime,
9 + required this.to,
10 + required this.from,
11 + required this.direction,
12 + required this.solAmount,
13 + this.tokenSymbol = "SOL",
14 + required this.isPending,
15 + required this.txFee,
16 + }) : amount = solAmount.toInt();
17 +
18 + final String id;
19 + final String? to;
20 + final String? from;
21 + final int amount;
22 + final bool isPending;
23 + final double solAmount;
24 + final String tokenSymbol;
25 + final DateTime blockTime;
26 + final double txFee;
27 + final TransactionDirection direction;
28 +
29 + String? _fiatAmount;
30 +
31 + @override
32 + DateTime get date => blockTime;
33 +
34 + @override
35 + String amountFormatted() {
36 + String stringBalance = solAmount.toString();
37 +
38 + if (stringBalance.toString().length >= 6) {
39 + stringBalance = stringBalance.substring(0, 6);
40 + }
41 + return '$stringBalance $tokenSymbol';
42 + }
43 +
44 + @override
45 + String fiatAmount() => _fiatAmount ?? '';
46 +
47 + @override
48 + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
49 +
50 + @override
51 + String feeFormatted() => '${txFee.toString()} SOL';
52 +
53 + factory SolanaTransactionInfo.fromJson(Map<String, dynamic> data) {
54 + return SolanaTransactionInfo(
55 + id: data['id'] as String,
56 + solAmount: data['solAmount'],
57 + direction: parseTransactionDirectionFromInt(data['direction'] as int),
58 + blockTime: DateTime.fromMillisecondsSinceEpoch(data['blockTime'] as int),
59 + isPending: data['isPending'] as bool,
60 + tokenSymbol: data['tokenSymbol'] as String,
61 + to: data['to'],
62 + from: data['from'],
63 + txFee: data['txFee'],
64 + );
65 + }
66 +
67 + Map<String, dynamic> toJson() => {
68 + 'id': id,
69 + 'solAmount': solAmount,
70 + 'direction': direction.index,
71 + 'blockTime': blockTime.millisecondsSinceEpoch,
72 + 'isPending': isPending,
73 + 'tokenSymbol': tokenSymbol,
74 + 'to': to,
75 + 'from': from,
76 + 'txFee': txFee,
77 + };
78 +}
cw_solana/lib/solana_transaction_model.dart new
+47
@@ -0,0 +1,47 @@
1 +class SolanaTransactionModel {
2 + final String id;
3 +
4 + final String from;
5 +
6 + final String to;
7 +
8 + final double amount;
9 +
10 + // If this is an outgoing transaction
11 + final bool isOutgoingTx;
12 +
13 + // The Program ID of this transaction, e.g, System Program, Token Program...
14 + final String programId;
15 +
16 + // The DateTime from the UNIX timestamp of the block where the transaction was included
17 + final DateTime blockTime;
18 +
19 + // The Transaction fee
20 + final double fee;
21 +
22 + // The token symbol
23 + final String tokenSymbol;
24 +
25 + SolanaTransactionModel({
26 + required this.id,
27 + required this.to,
28 + required this.from,
29 + required this.amount,
30 + required this.programId,
31 + required int blockTimeInInt,
32 + this.isOutgoingTx = false,
33 + required this.tokenSymbol,
34 + required this.fee,
35 + }) : blockTime = DateTime.fromMillisecondsSinceEpoch(blockTimeInInt * 1000);
36 +
37 + factory SolanaTransactionModel.fromJson(Map<String, dynamic> json) => SolanaTransactionModel(
38 + id: json['id'],
39 + blockTimeInInt: int.parse(json["timeStamp"]) * 1000,
40 + from: json["from"],
41 + to: json["to"],
42 + amount: double.parse(json["value"]),
43 + programId: json["programId"],
44 + fee: json['fee'],
45 + tokenSymbol: json['tokenSymbol'],
46 + );
47 +}
cw_solana/lib/solana_wallet.dart new
+510
@@ -0,0 +1,510 @@
1 +import 'dart:async';
2 +import 'dart:convert';
3 +import 'dart:developer';
4 +import 'dart:io';
5 +import 'package:cw_core/cake_hive.dart';
6 +import 'package:cw_core/crypto_currency.dart';
7 +import 'package:cw_core/node.dart';
8 +import 'package:cw_core/pathForWallet.dart';
9 +import 'package:cw_core/pending_transaction.dart';
10 +import 'package:cw_core/sync_status.dart';
11 +import 'package:cw_core/transaction_direction.dart';
12 +import 'package:cw_core/transaction_priority.dart';
13 +import 'package:cw_core/wallet_addresses.dart';
14 +import 'package:cw_core/wallet_base.dart';
15 +import 'package:cw_core/wallet_info.dart';
16 +import 'package:cw_solana/default_spl_tokens.dart';
17 +import 'package:cw_solana/file.dart';
18 +import 'package:cw_solana/solana_balance.dart';
19 +import 'package:cw_solana/solana_client.dart';
20 +import 'package:cw_solana/solana_exceptions.dart';
21 +import 'package:cw_solana/solana_transaction_credentials.dart';
22 +import 'package:cw_solana/solana_transaction_history.dart';
23 +import 'package:cw_solana/solana_transaction_info.dart';
24 +import 'package:cw_solana/solana_transaction_model.dart';
25 +import 'package:cw_solana/solana_wallet_addresses.dart';
26 +import 'package:cw_solana/spl_token.dart';
27 +import 'package:hex/hex.dart';
28 +import 'package:hive/hive.dart';
29 +import 'package:mobx/mobx.dart';
30 +import 'package:shared_preferences/shared_preferences.dart';
31 +import 'package:solana/metaplex.dart' as metaplex;
32 +import 'package:solana/solana.dart';
33 +import 'package:web3dart/crypto.dart';
34 +
35 +part 'solana_wallet.g.dart';
36 +
37 +class SolanaWallet = SolanaWalletBase with _$SolanaWallet;
38 +
39 +abstract class SolanaWalletBase
40 + extends WalletBase<SolanaBalance, SolanaTransactionHistory, SolanaTransactionInfo> with Store {
41 + SolanaWalletBase({
42 + required WalletInfo walletInfo,
43 + String? mnemonic,
44 + String? privateKey,
45 + required String password,
46 + SolanaBalance? initialBalance,
47 + }) : syncStatus = const NotConnectedSyncStatus(),
48 + _password = password,
49 + _mnemonic = mnemonic,
50 + _hexPrivateKey = privateKey,
51 + _client = SolanaWalletClient(),
52 + walletAddresses = SolanaWalletAddresses(walletInfo),
53 + balance = ObservableMap<CryptoCurrency, SolanaBalance>.of(
54 + {CryptoCurrency.sol: initialBalance ?? SolanaBalance(BigInt.zero.toDouble())}),
55 + super(walletInfo) {
56 + this.walletInfo = walletInfo;
57 + transactionHistory = SolanaTransactionHistory(walletInfo: walletInfo, password: password);
58 +
59 + if (!CakeHive.isAdapterRegistered(SPLToken.typeId)) {
60 + CakeHive.registerAdapter(SPLTokenAdapter());
61 + }
62 +
63 + _sharedPrefs.complete(SharedPreferences.getInstance());
64 + }
65 +
66 + final String _password;
67 + final String? _mnemonic;
68 + final String? _hexPrivateKey;
69 +
70 + // The Solana WalletPair
71 + Ed25519HDKeyPair? _walletKeyPair;
72 +
73 + Ed25519HDKeyPair? get walletKeyPair => _walletKeyPair;
74 +
75 + // To access the privateKey bytes.
76 + Ed25519HDKeyPairData? _keyPairData;
77 +
78 + late SolanaWalletClient _client;
79 +
80 + Timer? _transactionsUpdateTimer;
81 +
82 + late final Box<SPLToken> splTokensBox;
83 +
84 + @override
85 + WalletAddresses walletAddresses;
86 +
87 + @override
88 + @observable
89 + SyncStatus syncStatus;
90 +
91 + @override
92 + @observable
93 + late ObservableMap<CryptoCurrency, SolanaBalance> balance;
94 +
95 + Completer<SharedPreferences> _sharedPrefs = Completer();
96 +
97 + @override
98 + Ed25519HDKeyPairData get keys {
99 + if (_keyPairData == null) {
100 + return Ed25519HDKeyPairData([], publicKey: const Ed25519HDPublicKey([]));
101 + }
102 +
103 + return _keyPairData!;
104 + }
105 +
106 + @override
107 + String? get seed => _mnemonic;
108 +
109 + @override
110 + String get privateKey => HEX.encode(_keyPairData!.bytes);
111 +
112 + Future<void> init() async {
113 + final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${SPLToken.boxName}";
114 +
115 + splTokensBox = await CakeHive.openBox<SPLToken>(boxName);
116 +
117 + // Create WalletPair using either the mnemonic or the privateKey
118 + _walletKeyPair = await getWalletPair(
119 + mnemonic: _mnemonic,
120 + privateKey: _hexPrivateKey,
121 + );
122 +
123 + // Extract the keyPairData containing both the privateKey bytes and the publicKey hex.
124 + _keyPairData = await _walletKeyPair!.extract();
125 +
126 + walletInfo.address = _walletKeyPair!.address;
127 +
128 + await walletAddresses.init();
129 + await transactionHistory.init();
130 + await save();
131 + }
132 +
133 + Future<Wallet> getWalletPair({String? mnemonic, String? privateKey}) async {
134 + assert(mnemonic != null || privateKey != null);
135 +
136 + if (privateKey != null) {
137 + final privateKeyBytes = hexToBytes(privateKey);
138 + return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes);
139 + }
140 +
141 + return Wallet.fromMnemonic(mnemonic!, account: 0, change: 0);
142 + }
143 +
144 + @override
145 + int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0;
146 +
147 + @override
148 + Future<void> changePassword(String password) => throw UnimplementedError("changePassword");
149 +
150 + @override
151 + void close() {
152 + _client.stop();
153 + _transactionsUpdateTimer?.cancel();
154 + }
155 +
156 + @action
157 + @override
158 + Future<void> connectToNode({required Node node}) async {
159 + try {
160 + syncStatus = ConnectingSyncStatus();
161 +
162 + final isConnected = _client.connect(node);
163 +
164 + if (!isConnected) {
165 + throw Exception("Solana Node connection failed");
166 + }
167 +
168 + try {
169 + await Future.wait([
170 + _updateBalance(),
171 + _updateNativeSOLTransactions(),
172 + _updateSPLTokenTransactions(),
173 + ]);
174 + } catch (e) {
175 + log(e.toString());
176 + }
177 +
178 + _setTransactionUpdateTimer();
179 +
180 + syncStatus = ConnectedSyncStatus();
181 + } catch (e) {
182 + syncStatus = FailedSyncStatus();
183 + }
184 + }
185 +
186 + @override
187 + Future<PendingTransaction> createTransaction(Object credentials) async {
188 + final solCredentials = credentials as SolanaTransactionCredentials;
189 +
190 + final outputs = solCredentials.outputs;
191 +
192 + final hasMultiDestination = outputs.length > 1;
193 +
194 + await _updateBalance();
195 +
196 + final CryptoCurrency transactionCurrency =
197 + balance.keys.firstWhere((element) => element.title == solCredentials.currency.title);
198 +
199 + final walletBalanceForCurrency = balance[transactionCurrency]!.balance;
200 +
201 + double totalAmount = 0.0;
202 +
203 + if (hasMultiDestination) {
204 + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
205 + throw SolanaTransactionWrongBalanceException(transactionCurrency);
206 + }
207 +
208 + final totalAmountFromCredentials =
209 + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0));
210 +
211 + totalAmount = totalAmountFromCredentials.toDouble();
212 +
213 + if (walletBalanceForCurrency < totalAmount) {
214 + throw SolanaTransactionWrongBalanceException(transactionCurrency);
215 + }
216 + } else {
217 + final output = outputs.first;
218 +
219 + final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0');
220 +
221 + totalAmount = output.sendAll ? walletBalanceForCurrency : totalOriginalAmount;
222 +
223 + if (walletBalanceForCurrency < totalAmount) {
224 + throw SolanaTransactionWrongBalanceException(transactionCurrency);
225 + }
226 + }
227 +
228 + String? tokenMint;
229 + // Token Mint is only needed for transactions that are not native tokens(non-SOL transactions)
230 + if (transactionCurrency.title != CryptoCurrency.sol.title) {
231 + tokenMint = (transactionCurrency as SPLToken).mintAddress;
232 + }
233 +
234 + final pendingSolanaTransaction = await _client.signSolanaTransaction(
235 + tokenMint: tokenMint,
236 + tokenTitle: transactionCurrency.title,
237 + inputAmount: totalAmount,
238 + ownerKeypair: _walletKeyPair!,
239 + tokenDecimals: transactionCurrency.decimals,
240 + destinationAddress: solCredentials.outputs.first.isParsedAddress
241 + ? solCredentials.outputs.first.extractedAddress!
242 + : solCredentials.outputs.first.address,
243 + );
244 +
245 + return pendingSolanaTransaction;
246 + }
247 +
248 + @override
249 + Future<Map<String, SolanaTransactionInfo>> fetchTransactions() async => {};
250 +
251 + /// Fetches the native SOL transactions linked to the wallet Public Key
252 + Future<void> _updateNativeSOLTransactions() async {
253 + final address = Ed25519HDPublicKey.fromBase58(_walletKeyPair!.address);
254 +
255 + final transactions = await _client.fetchTransactions(address);
256 +
257 + final Map<String, SolanaTransactionInfo> result = {};
258 +
259 + for (var transactionModel in transactions) {
260 + result[transactionModel.id] = SolanaTransactionInfo(
261 + id: transactionModel.id,
262 + to: transactionModel.to,
263 + from: transactionModel.from,
264 + blockTime: transactionModel.blockTime,
265 + direction: transactionModel.isOutgoingTx
266 + ? TransactionDirection.outgoing
267 + : TransactionDirection.incoming,
268 + solAmount: transactionModel.amount,
269 + isPending: false,
270 + txFee: transactionModel.fee,
271 + tokenSymbol: transactionModel.tokenSymbol,
272 + );
273 + }
274 +
275 + transactionHistory.addMany(result);
276 +
277 + await transactionHistory.save();
278 + }
279 +
280 + /// Fetches the SPL Tokens transactions linked to the token account Public Key
281 + Future<void> _updateSPLTokenTransactions() async {
282 + List<SolanaTransactionModel> splTokenTransactions = [];
283 +
284 + for (var token in balance.keys) {
285 + if (token is SPLToken) {
286 + final tokenTxs = await _client.getSPLTokenTransfers(
287 + token.mintAddress,
288 + token.symbol,
289 + token.decimal,
290 + _walletKeyPair!,
291 + );
292 +
293 + splTokenTransactions.addAll(tokenTxs);
294 + }
295 + }
296 +
297 + final Map<String, SolanaTransactionInfo> result = {};
298 +
299 + for (var transactionModel in splTokenTransactions) {
300 + result[transactionModel.id] = SolanaTransactionInfo(
301 + id: transactionModel.id,
302 + to: transactionModel.to,
303 + from: transactionModel.from,
304 + blockTime: transactionModel.blockTime,
305 + direction: transactionModel.isOutgoingTx
306 + ? TransactionDirection.outgoing
307 + : TransactionDirection.incoming,
308 + solAmount: transactionModel.amount,
309 + isPending: false,
310 + txFee: transactionModel.fee,
311 + tokenSymbol: transactionModel.tokenSymbol,
312 + );
313 + }
314 +
315 + transactionHistory.addMany(result);
316 +
317 + await transactionHistory.save();
318 + }
319 +
320 + @override
321 + Future<void> rescan({required int height}) => throw UnimplementedError("rescan");
322 +
323 + @override
324 + Future<void> save() async {
325 + await walletAddresses.updateAddressesInBox();
326 + final path = await makePath();
327 + await write(path: path, password: _password, data: toJSON());
328 + await transactionHistory.save();
329 + }
330 +
331 + @action
332 + @override
333 + Future<void> startSync() async {
334 + try {
335 + syncStatus = AttemptingSyncStatus();
336 +
337 + await Future.wait([
338 + _updateBalance(),
339 + _updateNativeSOLTransactions(),
340 + _updateSPLTokenTransactions(),
341 + ]);
342 +
343 + syncStatus = SyncedSyncStatus();
344 + } catch (e) {
345 + syncStatus = FailedSyncStatus();
346 + }
347 + }
348 +
349 + Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
350 +
351 + String toJSON() => json.encode({
352 + 'mnemonic': _mnemonic,
353 + 'private_key': privateKey,
354 + 'balance': balance[currency]!.toJSON(),
355 + });
356 +
357 + static Future<SolanaWallet> open({
358 + required String name,
359 + required String password,
360 + required WalletInfo walletInfo,
361 + }) async {
362 + final path = await pathForWallet(name: name, type: walletInfo.type);
363 + final jsonSource = await read(path: path, password: password);
364 + final data = json.decode(jsonSource) as Map;
365 + final mnemonic = data['mnemonic'] as String?;
366 + final privateKey = data['private_key'] as String?;
367 + final balance = SolanaBalance.fromJSON(data['balance'] as String) ?? SolanaBalance(0.0);
368 +
369 + return SolanaWallet(
370 + walletInfo: walletInfo,
371 + password: password,
372 + mnemonic: mnemonic,
373 + privateKey: privateKey,
374 + initialBalance: balance,
375 + );
376 + }
377 +
378 + Future<void> _updateBalance() async {
379 + balance[currency] = await _fetchSOLBalance();
380 + await _fetchSPLTokensBalances();
381 + await save();
382 + }
383 +
384 + Future<SolanaBalance> _fetchSOLBalance() async {
385 + final balance = await _client.getBalance(_walletKeyPair!.address);
386 +
387 + return SolanaBalance(balance);
388 + }
389 +
390 + Future<void> _fetchSPLTokensBalances() async {
391 + for (var token in splTokensBox.values) {
392 + if (token.enabled) {
393 + try {
394 + final tokenBalance =
395 + await _client.getSplTokenBalance(token.mintAddress, _walletKeyPair!.address) ??
396 + balance[token] ??
397 + SolanaBalance(0.0);
398 + balance[token] = tokenBalance;
399 + } catch (e) {
400 + print('Error fetching spl token (${token.symbol}) balance ${e.toString()}');
401 + }
402 + } else {
403 + balance.remove(token);
404 + }
405 + }
406 + }
407 +
408 + @override
409 + Future<void>? updateBalance() async => await _updateBalance();
410 +
411 + List<SPLToken> get splTokenCurrencies => splTokensBox.values.toList();
412 +
413 + void addInitialTokens() {
414 + final initialSPLTokens = DefaultSPLTokens().initialSPLTokens;
415 +
416 + for (var token in initialSPLTokens) {
417 + splTokensBox.put(token.mintAddress, token);
418 + }
419 + }
420 +
421 + Future<void> addSPLToken(SPLToken token) async {
422 + await splTokensBox.put(token.mintAddress, token);
423 +
424 + if (token.enabled) {
425 + final tokenBalance =
426 + await _client.getSplTokenBalance(token.mintAddress, _walletKeyPair!.address) ??
427 + balance[token] ??
428 + SolanaBalance(0.0);
429 +
430 + balance[token] = tokenBalance;
431 + } else {
432 + balance.remove(token);
433 + }
434 + }
435 +
436 + Future<void> deleteSPLToken(SPLToken token) async {
437 + await token.delete();
438 +
439 + balance.remove(token);
440 + _updateBalance();
441 + }
442 +
443 + Future<SPLToken?> getSPLToken(String mintAddress) async {
444 + // Convert SPL token mint address to public key
445 + final mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress);
446 +
447 + // Fetch token's metadata account
448 + final token = await solanaClient!.rpcClient.getMetadata(mint: mintPublicKey);
449 +
450 + if (token == null) {
451 + return null;
452 + }
453 +
454 + return SPLToken.fromMetadata(
455 + name: token.name,
456 + mint: token.mint,
457 + symbol: token.symbol,
458 + mintAddress: mintAddress,
459 + );
460 + }
461 +
462 + @override
463 + Future<void> renameWalletFiles(String newWalletName) async {
464 + final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
465 + final currentWalletFile = File(currentWalletPath);
466 +
467 + final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
468 + final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName');
469 +
470 + // Copies current wallet files into new wallet name's dir and files
471 + if (currentWalletFile.existsSync()) {
472 + final newWalletPath = await pathForWallet(name: newWalletName, type: type);
473 + await currentWalletFile.copy(newWalletPath);
474 + }
475 + if (currentTransactionsFile.existsSync()) {
476 + final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
477 + await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName');
478 + }
479 +
480 + // Delete old name's dir and files
481 + await Directory(currentDirPath).delete(recursive: true);
482 + }
483 +
484 + void _setTransactionUpdateTimer() {
485 + if (_transactionsUpdateTimer?.isActive ?? false) {
486 + _transactionsUpdateTimer!.cancel();
487 + }
488 +
489 + _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) {
490 + _updateSPLTokenTransactions();
491 + _updateNativeSOLTransactions();
492 + _updateBalance();
493 + });
494 + }
495 +
496 + Future<String> signSolanaMessage(String message) async {
497 + // Convert the message to bytes
498 + final messageBytes = utf8.encode(message);
499 +
500 + // Sign the message bytes with the wallet's private key
501 + final signature = await _walletKeyPair!.sign(messageBytes);
502 +
503 + // Convert the signature to a hexadecimal string
504 + final hex = bytesToHex(signature.bytes);
505 +
506 + return hex;
507 + }
508 +
509 + SolanaClient? get solanaClient => _client.getSolanaClient;
510 +}
cw_solana/lib/solana_wallet_addresses.dart new
+33
@@ -0,0 +1,33 @@
1 +import 'package:cw_core/wallet_addresses.dart';
2 +import 'package:cw_core/wallet_info.dart';
3 +import 'package:mobx/mobx.dart';
4 +
5 +part 'solana_wallet_addresses.g.dart';
6 +
7 +class SolanaWalletAddresses = SolanaWalletAddressesBase with _$SolanaWalletAddresses;
8 +
9 +abstract class SolanaWalletAddressesBase extends WalletAddresses with Store {
10 + SolanaWalletAddressesBase(WalletInfo walletInfo)
11 + : address = '',
12 + super(walletInfo);
13 +
14 + @override
15 + String address;
16 +
17 + @override
18 + Future<void> init() async {
19 + address = walletInfo.address;
20 + await updateAddressesInBox();
21 + }
22 +
23 + @override
24 + Future<void> updateAddressesInBox() async {
25 + try {
26 + addressesMap.clear();
27 + addressesMap[address] = '';
28 + await saveAddressesInBox();
29 + } catch (e) {
30 + print(e.toString());
31 + }
32 + }
33 +}
cw_solana/lib/solana_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 SolanaNewWalletCredentials extends WalletCredentials {
5 + SolanaNewWalletCredentials({required String name, WalletInfo? walletInfo})
6 + : super(name: name, walletInfo: walletInfo);
7 +}
8 +
9 +class SolanaRestoreWalletFromSeedCredentials extends WalletCredentials {
10 + SolanaRestoreWalletFromSeedCredentials(
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 SolanaRestoreWalletFromPrivateKey extends WalletCredentials {
21 + SolanaRestoreWalletFromPrivateKey(
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_solana/lib/solana_wallet_service.dart new
+118
@@ -0,0 +1,118 @@
1 +import 'dart:io';
2 +
3 +import 'package:bip39/bip39.dart' as bip39;
4 +import 'package:collection/collection.dart';
5 +import 'package:cw_core/pathForWallet.dart';
6 +import 'package:cw_core/wallet_base.dart';
7 +import 'package:cw_core/wallet_info.dart';
8 +import 'package:cw_core/wallet_service.dart';
9 +import 'package:cw_core/wallet_type.dart';
10 +import 'package:cw_solana/solana_mnemonics.dart';
11 +import 'package:cw_solana/solana_wallet.dart';
12 +import 'package:cw_solana/solana_wallet_creation_credentials.dart';
13 +import 'package:hive/hive.dart';
14 +
15 +class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
16 + SolanaRestoreWalletFromSeedCredentials, SolanaRestoreWalletFromPrivateKey> {
17 + SolanaWalletService(this.walletInfoSource);
18 +
19 + final Box<WalletInfo> walletInfoSource;
20 +
21 + @override
22 + Future<SolanaWallet> create(SolanaNewWalletCredentials credentials) async {
23 + final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
24 +
25 + final mnemonic = bip39.generateMnemonic(strength: strength);
26 +
27 + final wallet = SolanaWallet(
28 + walletInfo: credentials.walletInfo!,
29 + mnemonic: mnemonic,
30 + password: credentials.password!,
31 + );
32 +
33 + await wallet.init();
34 + wallet.addInitialTokens();
35 + return wallet;
36 + }
37 +
38 + @override
39 + WalletType getType() => WalletType.solana;
40 +
41 + @override
42 + Future<bool> isWalletExit(String name) async =>
43 + File(await pathForWallet(name: name, type: getType())).existsSync();
44 +
45 + @override
46 + Future<SolanaWallet> openWallet(String name, String password) async {
47 + final walletInfo =
48 + walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
49 + final wallet = await SolanaWalletBase.open(
50 + name: name,
51 + password: password,
52 + walletInfo: walletInfo,
53 + );
54 +
55 + await wallet.init();
56 + await wallet.save();
57 +
58 + return wallet;
59 + }
60 +
61 + @override
62 + Future<void> remove(String wallet) async {
63 + File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
64 + final walletInfo = walletInfoSource.values
65 + .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
66 + await walletInfoSource.delete(walletInfo.key);
67 + }
68 +
69 + @override
70 + Future<SolanaWallet> restoreFromKeys(SolanaRestoreWalletFromPrivateKey credentials) async {
71 + final wallet = SolanaWallet(
72 + password: credentials.password!,
73 + privateKey: credentials.privateKey,
74 + walletInfo: credentials.walletInfo!,
75 + );
76 +
77 + await wallet.init();
78 + wallet.addInitialTokens();
79 + await wallet.save();
80 +
81 + return wallet;
82 + }
83 +
84 + @override
85 + Future<SolanaWallet> restoreFromSeed(SolanaRestoreWalletFromSeedCredentials credentials) async {
86 + if (!bip39.validateMnemonic(credentials.mnemonic)) {
87 + throw SolanaMnemonicIsIncorrectException();
88 + }
89 +
90 + final wallet = SolanaWallet(
91 + password: credentials.password!,
92 + mnemonic: credentials.mnemonic,
93 + walletInfo: credentials.walletInfo!,
94 + );
95 +
96 + await wallet.init();
97 + wallet.addInitialTokens();
98 + await wallet.save();
99 +
100 + return wallet;
101 + }
102 +
103 + @override
104 + Future<void> rename(String currentName, String password, String newName) async {
105 + final currentWalletInfo = walletInfoSource.values
106 + .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType()));
107 + final currentWallet = await SolanaWalletBase.open(
108 + password: password, name: currentName, walletInfo: currentWalletInfo);
109 +
110 + await currentWallet.renameWalletFiles(newName);
111 +
112 + final newWalletInfo = currentWalletInfo;
113 + newWalletInfo.id = WalletBase.idFor(newName, getType());
114 + newWalletInfo.name = newName;
115 +
116 + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo);
117 + }
118 +}
cw_solana/lib/spl_token.dart new
+146
@@ -0,0 +1,146 @@
1 +import 'package:cw_core/crypto_currency.dart';
2 +import 'package:cw_core/hive_type_ids.dart';
3 +import 'package:hive/hive.dart';
4 +import 'package:solana/metaplex.dart';
5 +
6 +part 'spl_token.g.dart';
7 +
8 +@HiveType(typeId: SPLToken.typeId)
9 +class SPLToken extends CryptoCurrency with HiveObjectMixin {
10 + @HiveField(0)
11 + final String name;
12 +
13 + @HiveField(1)
14 + final String symbol;
15 +
16 + @HiveField(2)
17 + final String mintAddress;
18 +
19 + @HiveField(3)
20 + final int decimal;
21 +
22 + @HiveField(4, defaultValue: false)
23 + bool _enabled;
24 +
25 + @HiveField(5)
26 + final String mint;
27 +
28 + @HiveField(6)
29 + final String? iconPath;
30 +
31 + @HiveField(7)
32 + final String? tag;
33 +
34 + SPLToken({
35 + required this.name,
36 + required this.symbol,
37 + required this.mintAddress,
38 + required this.decimal,
39 + required this.mint,
40 + this.iconPath,
41 + this.tag = 'SOL',
42 + bool enabled = false,
43 + }) : _enabled = enabled,
44 + super(
45 + name: mint.toLowerCase(),
46 + title: symbol.toUpperCase(),
47 + fullName: name,
48 + tag: tag,
49 + iconPath: iconPath,
50 + decimals: decimal,
51 + );
52 +
53 + factory SPLToken.fromMetadata({
54 + required String name,
55 + required String mint,
56 + required String symbol,
57 + required String mintAddress,
58 + }) {
59 + return SPLToken(
60 + name: name,
61 + symbol: symbol,
62 + mintAddress: mintAddress,
63 + decimal: 0,
64 + mint: mint,
65 + iconPath: '',
66 + );
67 + }
68 +
69 + factory SPLToken.cryptoCurrency({
70 + required String name,
71 + required String symbol,
72 + required int decimals,
73 + required String iconPath,
74 + required String mint,
75 + }) {
76 + return SPLToken(
77 + name: name,
78 + symbol: symbol,
79 + decimal: decimals,
80 + mint: mint,
81 + iconPath: iconPath,
82 + mintAddress: '',
83 + );
84 + }
85 +
86 + bool get enabled => _enabled;
87 +
88 + set enabled(bool value) => _enabled = value;
89 +
90 + SPLToken.copyWith(SPLToken other, String? icon, String? tag)
91 + : name = other.name,
92 + symbol = other.symbol,
93 + mintAddress = other.mintAddress,
94 + decimal = other.decimal,
95 + _enabled = other.enabled,
96 + mint = other.mint,
97 + tag = other.tag,
98 + iconPath = icon,
99 + super(
100 + title: other.symbol.toUpperCase(),
101 + name: other.symbol.toLowerCase(),
102 + decimals: other.decimal,
103 + fullName: other.name,
104 + tag: other.tag,
105 + iconPath: icon,
106 + );
107 +
108 + static const typeId = SPL_TOKEN_TYPE_ID;
109 + static const boxName = 'SPLTokens';
110 +
111 + @override
112 + bool operator ==(other) =>
113 + (other is SPLToken && other.mintAddress == mintAddress) ||
114 + (other is CryptoCurrency && other.title == title);
115 +
116 + @override
117 + int get hashCode => mintAddress.hashCode;
118 +}
119 +
120 +class NFT extends SPLToken {
121 + final ImageInfo? imageInfo;
122 +
123 + NFT(
124 + String mint,
125 + String name,
126 + String symbol,
127 + String mintAddress,
128 + int decimal,
129 + String iconPath,
130 + this.imageInfo,
131 + ) : super(
132 + name: name,
133 + symbol: symbol,
134 + mintAddress: mintAddress,
135 + decimal: decimal,
136 + mint: mint,
137 + iconPath: iconPath,
138 + );
139 +}
140 +
141 +class ImageInfo {
142 + final String uri;
143 + final OffChainMetadata? data;
144 +
145 + const ImageInfo(this.uri, this.data);
146 +}
cw_solana/pubspec.yaml new
+37
@@ -0,0 +1,37 @@
1 +name: cw_solana
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 + solana: ^0.30.1
15 + cw_core:
16 + path: ../cw_core
17 + http: ^1.1.0
18 + hive: ^2.2.3
19 + bip39: ^1.0.6
20 + mobx: ^2.3.0+1
21 + shared_preferences: ^2.0.15
22 + web3dart: ^2.7.1
23 + bip32: ^2.0.0
24 + hex: ^0.2.0
25 +
26 +dev_dependencies:
27 + flutter_test:
28 + sdk: flutter
29 + flutter_lints: ^2.0.0
30 + build_runner: ^2.1.11
31 + mobx_codegen: ^2.0.7
32 + hive_generator: ^1.1.3
33 +
34 +flutter:
35 + # assets:
36 + # - images/a_dot_burr.jpeg
37 + # - images/a_dot_ham.jpeg
\ No newline at end of file
cw_solana/test/cw_solana_test.dart new
+12
@@ -0,0 +1,12 @@
1 +import 'package:flutter_test/flutter_test.dart';
2 +
3 +import 'package:cw_solana/cw_solana.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 +}
ios/Runner/InfoBase.plist
+10
@@ -180,6 +180,16 @@
180 <string>polygon-wallet</string>
181 </array>
182 </dict>
183 + <dict>
184 + <key>CFBundleTypeRole</key>
185 + <string>Viewer</string>
186 + <key>CFBundleURLName</key>
187 + <string>solana-wallet</string>
188 + <key>CFBundleURLSchemes</key>
189 + <array>
190 + <string>solana-wallet</string>
191 + </array>
192 + </dict>
193 </array>
194 <key>CFBundleVersion</key>
195 <string>$(CURRENT_PROJECT_VERSION)</string>
lib/core/address_validator.dart
+12 -5
@@ -1,6 +1,7 @@
1 import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
2 import 'package:cake_wallet/generated/i18n.dart';
3 import 'package:cake_wallet/core/validator.dart';
4 +import 'package:cake_wallet/solana/solana.dart';
5 import 'package:cw_core/crypto_currency.dart';
6 import 'package:cw_core/erc20_token.dart';
7
@@ -8,9 +9,7 @@ class AddressValidator extends TextValidator {
9 AddressValidator({required CryptoCurrency type})
10 : super(
11 errorMessage: S.current.error_text_address,
11 - useAdditionalValidation: type == CryptoCurrency.btc
12 - ? bitcoin.Address.validateAddress
13 - : null,
12 + useAdditionalValidation: type == CryptoCurrency.btc ? bitcoin.Address.validateAddress : null,
13 pattern: getPattern(type),
14 length: getLength(type));
15
@@ -130,6 +129,12 @@ class AddressValidator extends TextValidator {
129 if (type is Erc20Token) {
130 return [42];
131 }
132 +
133 + if (solana != null) {
134 + final length = solana!.getValidationLength(type);
135 + if (length != null) return length;
136 + }
137 +
138 switch (type) {
139 case CryptoCurrency.xmr:
140 return null;
@@ -192,11 +197,11 @@ class AddressValidator extends TextValidator {
197 case CryptoCurrency.sc:
198 return [76];
199 case CryptoCurrency.sol:
200 + case CryptoCurrency.usdtSol:
201 + case CryptoCurrency.usdcsol:
202 return [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44];
203 case CryptoCurrency.trx:
204 return [34];
198 - case CryptoCurrency.usdcsol:
199 - return [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44];
205 case CryptoCurrency.usdt:
206 return [34];
207 case CryptoCurrency.usdttrc20:
@@ -286,6 +291,8 @@ class AddressValidator extends TextValidator {
291 '|bitcoincash:q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)'
292 '|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{41}([^0-9a-zA-Z]|\$)'
293 '|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)';
294 + case CryptoCurrency.sol:
295 + return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)';
296 default:
297 return null;
298 }
lib/core/seed_validator.dart
+3
@@ -4,6 +4,7 @@ import 'package:cake_wallet/haven/haven.dart';
4 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:cw_core/wallet_type.dart';
9 import 'package:cake_wallet/monero/monero.dart';
10 import 'package:cake_wallet/nano/nano.dart';
@@ -37,6 +38,8 @@ class SeedValidator extends Validator<MnemonicItem> {
38 return nano!.getNanoWordList(language);
39 case WalletType.polygon:
40 return polygon!.getPolygonWordList(language);
41 + case WalletType.solana:
42 + return solana!.getSolanaWordList(language);
43 default:
44 return [];
45 }
lib/core/wallet_connect/chain_service/chain_service.dart renamed
lib/core/wallet_connect/chain_service/eth/evm_chain_id.dart renamed
+1 -1
@@ -1,4 +1,4 @@
1 -import 'package:cake_wallet/core/wallet_connect/evm_chain_service.dart';
1 +import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_service.dart';
2
3 enum EVMChainId {
4 ethereum,
lib/core/wallet_connect/chain_service/eth/evm_chain_service.dart renamed
+3 -3
@@ -3,7 +3,7 @@ import 'dart:developer';
3 import 'dart:typed_data';
4
5 import 'package:cake_wallet/core/wallet_connect/eth_transaction_model.dart';
6 -import 'package:cake_wallet/core/wallet_connect/evm_chain_id.dart';
6 +import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart';
7 import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
8 import 'package:cake_wallet/generated/i18n.dart';
9 import 'package:cake_wallet/reactions/wallet_connect.dart';
@@ -20,8 +20,8 @@ import 'package:eth_sig_util/util/utils.dart';
20 import 'package:http/http.dart' as http;
21 import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
22 import 'package:web3dart/web3dart.dart';
23 -import 'chain_service.dart';
24 -import 'wallet_connect_key_service.dart';
23 +import '../chain_service.dart';
24 +import '../../wallet_connect_key_service.dart';
25
26 class EvmChainServiceImpl implements ChainService {
27 final AppStore appStore;
lib/core/wallet_connect/chain_service/solana/entities/solana_sign_message.dart new
+28
@@ -0,0 +1,28 @@
1 +class SolanaSignMessage {
2 + final String pubkey;
3 + final String message;
4 +
5 + SolanaSignMessage({
6 + required this.pubkey,
7 + required this.message,
8 + });
9 +
10 + factory SolanaSignMessage.fromJson(Map<String, dynamic> json) {
11 + return SolanaSignMessage(
12 + pubkey: json['pubkey'] as String,
13 + message: json['message'] as String,
14 + );
15 + }
16 +
17 + Map<String, dynamic> toJson() {
18 + return <String, dynamic>{
19 + 'pubkey': pubkey,
20 + 'message': message,
21 + };
22 + }
23 +
24 + @override
25 + String toString() {
26 + return 'SolanaSignMessage(pubkey: $pubkey, message: $message)';
27 + }
28 +}
lib/core/wallet_connect/chain_service/solana/entities/solana_sign_transaction.dart new
+106
@@ -0,0 +1,106 @@
1 +class SolanaSignTransaction {
2 + final String? feePayer;
3 + final String? recentBlockhash;
4 + final String transaction;
5 + final List<SolanaInstruction>? instructions;
6 +
7 + SolanaSignTransaction({
8 + required this.feePayer,
9 + required this.recentBlockhash,
10 + required this.instructions,
11 + required this.transaction,
12 + });
13 +
14 + factory SolanaSignTransaction.fromJson(Map<String, dynamic> json) {
15 + return SolanaSignTransaction(
16 + feePayer:json['feePayer'] !=null ? json['feePayer'] as String: null,
17 + recentBlockhash: json['recentBlockhash']!=null? json['recentBlockhash'] as String: null,
18 + instructions:json['instructions']!=null? (json['instructions'] as List<dynamic>)
19 + .map((e) => SolanaInstruction.fromJson(e as Map<String, dynamic>))
20 + .toList(): null,
21 + transaction: json['transaction'] as String,
22 + );
23 + }
24 +
25 + Map<String, dynamic> toJson() {
26 + return {
27 + 'feePayer': feePayer,
28 + 'recentBlockhash': recentBlockhash,
29 + 'instructions': instructions,
30 + 'transaction': transaction,
31 + };
32 + }
33 +
34 + @override
35 + String toString() {
36 + return 'SolanaSignTransaction(feePayer: $feePayer, recentBlockhash: $recentBlockhash, instructions: $instructions, transaction: $transaction)';
37 + }
38 +}
39 +
40 +class SolanaInstruction {
41 + final String programId;
42 + final List<SolanaKeyMetadata> keys;
43 + final List<int> data;
44 +
45 + SolanaInstruction({
46 + required this.programId,
47 + required this.keys,
48 + required this.data,
49 + });
50 +
51 + factory SolanaInstruction.fromJson(Map<String, dynamic> json) {
52 + return SolanaInstruction(
53 + programId: json['programId'] as String,
54 + keys: (json['keys'] as List<dynamic>)
55 + .map((e) => SolanaKeyMetadata.fromJson(e as Map<String, dynamic>))
56 + .toList(),
57 + data: (json['data'] as List<dynamic>).map((e) => e as int).toList(),
58 + );
59 + }
60 +
61 + Map<String, dynamic> toJson() {
62 + return <String, dynamic>{
63 + 'programId': programId,
64 + 'keys': keys,
65 + 'data': data,
66 + };
67 + }
68 +
69 + @override
70 + String toString() {
71 + return 'SolanaInstruction(programId: $programId, keys: $keys, data: $data)';
72 + }
73 +}
74 +
75 +class SolanaKeyMetadata {
76 + final String pubkey;
77 + final bool isSigner;
78 + final bool isWritable;
79 +
80 + SolanaKeyMetadata({
81 + required this.pubkey,
82 + required this.isSigner,
83 + required this.isWritable,
84 + });
85 +
86 + factory SolanaKeyMetadata.fromJson(Map<String, dynamic> json) {
87 + return SolanaKeyMetadata(
88 + pubkey: json['pubkey'] as String,
89 + isSigner: json['isSigner'] as bool,
90 + isWritable: json['isWritable'] as bool,
91 + );
92 + }
93 +
94 + Map<String, dynamic> toJson() {
95 + return <String, dynamic>{
96 + 'pubkey': pubkey,
97 + 'isSigner': isSigner,
98 + 'isWritable': isWritable,
99 + };
100 + }
101 +
102 + @override
103 + String toString() {
104 + return 'SolanaKeyMetadata(pubkey: $pubkey, isSigner: $isSigner, isWritable: $isWritable)';
105 + }
106 +}
lib/core/wallet_connect/chain_service/solana/solana_chain_id.dart new
+27
@@ -0,0 +1,27 @@
1 +import 'solana_chain_service.dart';
2 +
3 +enum SolanaChainId {
4 + mainnet,
5 + testnet,
6 + devnet,
7 +}
8 +
9 +extension SolanaChainIdX on SolanaChainId {
10 + String chain() {
11 + String name = '';
12 +
13 + switch (this) {
14 + case SolanaChainId.mainnet:
15 + name = '4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ';
16 + break;
17 + case SolanaChainId.testnet:
18 + name = '8E9rvCKLFQia2Y35HXjjpWzj8weVo44K';
19 + break;
20 + case SolanaChainId.devnet:
21 + name = '';
22 + break;
23 + }
24 +
25 + return '${SolanaChainServiceImpl.namespace}:$name';
26 + }
27 +}
lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart new
+177
@@ -0,0 +1,177 @@
1 +import 'dart:developer';
2 +
3 +import 'package:cake_wallet/core/wallet_connect/chain_service/solana/entities/solana_sign_message.dart';
4 +import 'package:cake_wallet/core/wallet_connect/chain_service/solana/solana_chain_id.dart';
5 +import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
6 +import 'package:cake_wallet/generated/i18n.dart';
7 +import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart';
8 +import 'package:cake_wallet/core/wallet_connect/models/connection_model.dart';
9 +import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_widget.dart';
10 +import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart';
11 +import 'package:solana/base58.dart';
12 +import 'package:solana/solana.dart';
13 +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
14 +import '../chain_service.dart';
15 +import '../../wallet_connect_key_service.dart';
16 +import 'entities/solana_sign_transaction.dart';
17 +
18 +class SolanaChainServiceImpl implements ChainService {
19 + final BottomSheetService bottomSheetService;
20 + final Web3Wallet wallet;
21 + final WalletConnectKeyService wcKeyService;
22 +
23 + static const namespace = 'solana';
24 + static const solSignTransaction = 'solana_signTransaction';
25 + static const solSignMessage = 'solana_signMessage';
26 +
27 + final SolanaChainId reference;
28 +
29 + final SolanaClient solanaClient;
30 +
31 + final Ed25519HDKeyPair? ownerKeyPair;
32 +
33 + SolanaChainServiceImpl({
34 + required this.reference,
35 + required this.wcKeyService,
36 + required this.bottomSheetService,
37 + required this.wallet,
38 + required this.ownerKeyPair,
39 + required String webSocketUrl,
40 + required Uri rpcUrl,
41 + SolanaClient? solanaClient,
42 + }) : solanaClient = solanaClient ??
43 + SolanaClient(
44 + rpcUrl: rpcUrl,
45 + websocketUrl: Uri.parse(webSocketUrl),
46 + timeout: const Duration(minutes: 2),
47 + ) {
48 + for (final String event in getEvents()) {
49 + wallet.registerEventEmitter(chainId: getChainId(), event: event);
50 + }
51 + wallet.registerRequestHandler(
52 + chainId: getChainId(),
53 + method: solSignTransaction,
54 + handler: solanaSignTransaction,
55 + );
56 + wallet.registerRequestHandler(
57 + chainId: getChainId(),
58 + method: solSignMessage,
59 + handler: solanaSignMessage,
60 + );
61 + }
62 +
63 + @override
64 + String getNamespace() {
65 + return namespace;
66 + }
67 +
68 + @override
69 + String getChainId() {
70 + return reference.chain();
71 + }
72 +
73 + @override
74 + List<String> getEvents() {
75 + return [''];
76 + }
77 +
78 + Future<String?> requestAuthorization(String? text) async {
79 + // Show the bottom sheet
80 + final bool? isApproved = await bottomSheetService.queueBottomSheet(
81 + widget: Web3RequestModal(
82 + child: ConnectionWidget(
83 + title: S.current.signTransaction,
84 + info: [
85 + ConnectionModel(
86 + text: text,
87 + ),
88 + ],
89 + ),
90 + ),
91 + ) as bool?;
92 +
93 + if (isApproved != null && isApproved == false) {
94 + return 'User rejected signature';
95 + }
96 +
97 + return null;
98 + }
99 +
100 + Future<String> solanaSignTransaction(String topic, dynamic parameters) async {
101 + log('received solana sign transaction request $parameters');
102 +
103 + final solanaSignTx =
104 + SolanaSignTransaction.fromJson(parameters as Map<String, dynamic>);
105 +
106 + final String? authError = await requestAuthorization('Confirm request to sign transaction?');
107 +
108 + if (authError != null) {
109 + return authError;
110 + }
111 +
112 + try {
113 + final message =
114 + await solanaClient.rpcClient.getMessageFromEncodedTx(solanaSignTx.transaction);
115 +
116 + final sign = await ownerKeyPair?.signMessage(
117 + message: message,
118 + recentBlockhash: solanaSignTx.recentBlockhash ?? '',
119 + );
120 +
121 + if (sign == null) {
122 + return '';
123 + }
124 +
125 + String signature = sign.signatures.first.toBase58();
126 +
127 + print(signature);
128 + print(signature.runtimeType);
129 +
130 + bottomSheetService.queueBottomSheet(
131 + isModalDismissible: true,
132 + widget: BottomSheetMessageDisplayWidget(
133 + message: S.current.awaitDAppProcessing,
134 + isError: false,
135 + ),
136 + );
137 +
138 + return signature;
139 + } catch (e) {
140 + log('An error has occurred while signing transaction: ${e.toString()}');
141 + bottomSheetService.queueBottomSheet(
142 + isModalDismissible: true,
143 + widget: BottomSheetMessageDisplayWidget(
144 + message: '${S.current.errorSigningTransaction}: ${e.toString()}',
145 + ),
146 + );
147 + return 'Failed';
148 + }
149 + }
150 +
151 + Future<String> solanaSignMessage(String topic, dynamic parameters) async {
152 + log('received solana sign message request: $parameters');
153 +
154 + final solanaSignMessage = SolanaSignMessage.fromJson(parameters as Map<String, dynamic>);
155 +
156 + final String? authError = await requestAuthorization('Confirm request to sign message?');
157 +
158 + if (authError != null) {
159 + return authError;
160 + }
161 + Signature? sign;
162 +
163 + try {
164 + sign = await ownerKeyPair?.sign(base58decode(solanaSignMessage.message));
165 + } catch (e) {
166 + print(e);
167 + }
168 +
169 + if (sign == null) {
170 + return '';
171 + }
172 +
173 + String signature = sign.toBase58();
174 +
175 + return signature;
176 + }
177 +}
lib/core/wallet_connect/wallet_connect_key_service.dart
+13 -1
@@ -2,6 +2,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart';
2 import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart';
3 import 'package:cake_wallet/polygon/polygon.dart';
4 import 'package:cake_wallet/reactions/wallet_connect.dart';
5 +import 'package:cake_wallet/solana/solana.dart';
6 import 'package:cw_core/wallet_base.dart';
7 import 'package:cw_core/wallet_type.dart';
8
@@ -13,7 +14,6 @@ abstract class WalletConnectKeyService {
14 /// If the chain is not found, returns an empty list.
15 /// - [chain]: The chain to get the keys for.
16 List<ChainKeyModel> getKeysForChain(WalletBase wallet);
16 -
17 }
18
19 class KeyServiceImpl implements WalletConnectKeyService {
@@ -23,6 +23,8 @@ class KeyServiceImpl implements WalletConnectKeyService {
23 return ethereum!.getPrivateKey(wallet);
24 case WalletType.polygon:
25 return polygon!.getPrivateKey(wallet);
26 + case WalletType.solana:
27 + return solana!.getPrivateKey(wallet);
28 default:
29 return '';
30 }
@@ -34,6 +36,8 @@ class KeyServiceImpl implements WalletConnectKeyService {
36 return ethereum!.getPublicKey(wallet);
37 case WalletType.polygon:
38 return polygon!.getPublicKey(wallet);
39 + case WalletType.solana:
40 + return solana!.getPublicKey(wallet);
41 default:
42 return '';
43 }
@@ -53,6 +57,14 @@ class KeyServiceImpl implements WalletConnectKeyService {
57 privateKey: _getPrivateKeyForWallet(wallet),
58 publicKey: _getPublicKeyForWallet(wallet),
59 ),
60 + ChainKeyModel(
61 + chains: [
62 + 'solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ', // main-net
63 + 'solana:8E9rvCKLFQia2Y35HXjjpWzj8weVo44K', // test-net
64 + ],
65 + privateKey: _getPrivateKeyForWallet(wallet),
66 + publicKey: _getPublicKeyForWallet(wallet),
67 + ),
68 ];
69 return keys;
70 }
lib/core/wallet_connect/web3wallet_service.dart
+34 -10
@@ -2,23 +2,27 @@ import 'dart:async';
2 import 'dart:developer';
3 import 'dart:typed_data';
4
5 -import 'package:cake_wallet/core/wallet_connect/evm_chain_id.dart';
6 -import 'package:cake_wallet/core/wallet_connect/evm_chain_service.dart';
5 +import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart';
6 +import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_service.dart';
7 import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart';
8 import 'package:cake_wallet/generated/i18n.dart';
9 import 'package:cake_wallet/core/wallet_connect/models/auth_request_model.dart';
10 import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart';
11 import 'package:cake_wallet/core/wallet_connect/models/session_request_model.dart';
12 import 'package:cake_wallet/reactions/wallet_connect.dart';
13 +import 'package:cake_wallet/solana/solana.dart';
14 import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_request_widget.dart';
15 import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart';
16 import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart';
17 import 'package:cake_wallet/store/app_store.dart';
18 +import 'package:cw_core/wallet_type.dart';
19 import 'package:eth_sig_util/eth_sig_util.dart';
20 import 'package:flutter/material.dart';
21 import 'package:mobx/mobx.dart';
22 import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
23
24 +import 'chain_service/solana/solana_chain_id.dart';
25 +import 'chain_service/solana/solana_chain_service.dart';
26 import 'wc_bottom_sheet_service.dart';
27 import 'package:cake_wallet/.secrets.g.dart' as secrets;
28
@@ -114,14 +118,34 @@ abstract class Web3WalletServiceBase with Store {
118 final newAuthRequests = _web3Wallet.completeRequests.getAll();
119 auth.addAll(newAuthRequests);
120
117 - for (final cId in EVMChainId.values) {
118 - EvmChainServiceImpl(
119 - reference: cId,
120 - appStore: appStore,
121 - wcKeyService: walletKeyService,
122 - bottomSheetService: _bottomSheetHandler,
123 - wallet: _web3Wallet,
124 - );
121 + if (isEVMCompatibleChain(appStore.wallet!.type)) {
122 + for (final cId in EVMChainId.values) {
123 + EvmChainServiceImpl(
124 + reference: cId,
125 + appStore: appStore,
126 + wcKeyService: walletKeyService,
127 + bottomSheetService: _bottomSheetHandler,
128 + wallet: _web3Wallet,
129 + );
130 + }
131 + }
132 +
133 + if (appStore.wallet!.type == WalletType.solana) {
134 + for (final cId in SolanaChainId.values) {
135 + final node = appStore.settingsStore.getCurrentNode(appStore.wallet!.type);
136 + final rpcUri = node.uri;
137 + final webSocketUri = 'wss://${node.uriRaw}/ws${node.uri.path}';
138 +
139 + SolanaChainServiceImpl(
140 + reference: cId,
141 + rpcUrl: rpcUri,
142 + webSocketUrl: webSocketUri,
143 + wcKeyService: walletKeyService,
144 + bottomSheetService: _bottomSheetHandler,
145 + wallet: _web3Wallet,
146 + ownerKeyPair: solana!.getWalletKeyPair(appStore.wallet!),
147 + );
148 + }
149 }
150 }
151
lib/di.dart
+4 -1
@@ -21,6 +21,7 @@ import 'package:cake_wallet/ionia/ionia_gift_card.dart';
21 import 'package:cake_wallet/ionia/ionia_tip.dart';
22 import 'package:cake_wallet/polygon/polygon.dart';
23 import 'package:cake_wallet/routes.dart';
24 +import 'package:cake_wallet/solana/solana.dart';
25 import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dart';
26 import 'package:cake_wallet/src/screens/buy/buy_options_page.dart';
27 import 'package:cake_wallet/src/screens/buy/webview_page.dart';
@@ -863,6 +864,8 @@ Future<void> setup({
864 return nano!.createNanoWalletService(_walletInfoSource);
865 case WalletType.polygon:
866 return polygon!.createPolygonWalletService(_walletInfoSource);
867 + case WalletType.solana:
868 + return solana!.createSolanaWalletService(_walletInfoSource);
869 default:
870 throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService');
871 }
@@ -1174,7 +1177,7 @@ Future<void> setup({
1177 getIt.registerFactoryParam<EditTokenPage, HomeSettingsViewModel, Map<String, dynamic>>(
1178 (homeSettingsViewModel, arguments) => EditTokenPage(
1179 homeSettingsViewModel: homeSettingsViewModel,
1177 - erc20token: arguments['token'] as Erc20Token?,
1180 + token: arguments['token'] as CryptoCurrency?,
1181 initialContractAddress: arguments['contractAddress'] as String?,
1182 ),
1183 );
lib/entities/default_settings_migration.dart
+41 -4
@@ -30,6 +30,7 @@ const polygonDefaultNodeUri = 'polygon-bor.publicnode.com';
30 const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002';
31 const nanoDefaultNodeUri = 'rpc.nano.to';
32 const nanoDefaultPowNodeUri = 'rpc.nano.to';
33 +const solanaDefaultNodeUri = 'rpc.ankr.com';
34
35 Future<void> defaultSettingsMigration(
36 {required int version,
@@ -186,10 +187,15 @@ Future<void> defaultSettingsMigration(
187 await rewriteSecureStoragePin(secureStorage: secureStorage);
188 break;
189 case 26:
189 - /// commented out as it was a probable cause for some users to have white screen issues
190 - /// maybe due to multiple access on Secure Storage at once
191 - /// or long await time on start of the app
192 - // await insecureStorageMigration(secureStorage: secureStorage, sharedPreferences: sharedPreferences);
190 +
191 + /// commented out as it was a probable cause for some users to have white screen issues
192 + /// maybe due to multiple access on Secure Storage at once
193 + /// or long await time on start of the app
194 + // await insecureStorageMigration(secureStorage: secureStorage, sharedPreferences: sharedPreferences);
195 + case 27:
196 + await addSolanaNodeList(nodes: nodes);
197 + await changeSolanaCurrentNodeToDefault(
198 + sharedPreferences: sharedPreferences, nodes: nodes);
199 break;
200 default:
201 break;
@@ -384,6 +390,11 @@ Node getMoneroDefaultNode({required Box<Node> nodes}) {
390 }
391 }
392
393 +Node? getSolanaDefaultNode({required Box<Node> nodes}) {
394 + return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == solanaDefaultNodeUri) ??
395 + nodes.values.firstWhereOrNull((node) => node.type == WalletType.solana);
396 +}
397 +
398 Future<void> insecureStorageMigration({
399 required SharedPreferences sharedPreferences,
400 required FlutterSecureStorage secureStorage,
@@ -673,6 +684,7 @@ Future<void> checkCurrentNodes(
684 final currentNanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey);
685 final currentBitcoinCashNodeId =
686 sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey);
687 + final currentSolanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey);
688 final currentMoneroNode =
689 nodeSource.values.firstWhereOrNull((node) => node.key == currentMoneroNodeId);
690 final currentBitcoinElectrumServer =
@@ -691,6 +703,8 @@ Future<void> checkCurrentNodes(
703 powNodeSource.values.firstWhereOrNull((node) => node.key == currentNanoPowNodeId);
704 final currentBitcoinCashNodeServer =
705 nodeSource.values.firstWhereOrNull((node) => node.key == currentBitcoinCashNodeId);
706 + final currentSolanaNodeServer =
707 + nodeSource.values.firstWhereOrNull((node) => node.key == currentSolanaNodeId);
708 if (currentMoneroNode == null) {
709 final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero);
710 await nodeSource.add(newCakeWalletNode);
@@ -750,6 +764,12 @@ Future<void> checkCurrentNodes(
764 await nodeSource.add(node);
765 await sharedPreferences.setInt(PreferencesKey.currentPolygonNodeIdKey, node.key as int);
766 }
767 +
768 + if (currentSolanaNodeServer == null) {
769 + final node = Node(uri: solanaDefaultNodeUri, type: WalletType.solana);
770 + await nodeSource.add(node);
771 + await sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, node.key as int);
772 + }
773 }
774
775 Future<void> resetBitcoinElectrumServer(
@@ -861,3 +881,20 @@ Future<void> changePolygonCurrentNodeToDefault(
881
882 await sharedPreferences.setInt(PreferencesKey.currentPolygonNodeIdKey, nodeId);
883 }
884 +
885 +Future<void> addSolanaNodeList({required Box<Node> nodes}) async {
886 + final nodeList = await loadDefaultSolanaNodes();
887 + for (var node in nodeList) {
888 + if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) {
889 + await nodes.add(node);
890 + }
891 + }
892 +}
893 +
894 +Future<void> changeSolanaCurrentNodeToDefault(
895 + {required SharedPreferences sharedPreferences, required Box<Node> nodes}) async {
896 + final node = getSolanaDefaultNode(nodes: nodes);
897 + final nodeId = node?.key as int? ?? 0;
898 +
899 + await sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, nodeId);
900 +}
lib/entities/node_list.dart
+20 -1
@@ -149,6 +149,23 @@ Future<List<Node>> loadDefaultPolygonNodes() async {
149 return nodes;
150 }
151
152 +Future<List<Node>> loadDefaultSolanaNodes() async {
153 + final nodesRaw = await rootBundle.loadString('assets/solana_node_list.yml');
154 + final loadedNodes = loadYaml(nodesRaw) as YamlList;
155 + final nodes = <Node>[];
156 +
157 + for (final raw in loadedNodes) {
158 + if (raw is Map) {
159 + final node = Node.fromMap(Map<String, Object>.from(raw));
160 +
161 + node.type = WalletType.solana;
162 + nodes.add(node);
163 + }
164 + }
165 +
166 + return nodes;
167 +}
168 +
169 Future<void> resetToDefault(Box<Node> nodeSource) async {
170 final moneroNodes = await loadDefaultNodes();
171 final bitcoinElectrumServerList = await loadBitcoinElectrumServerList();
@@ -158,6 +175,7 @@ Future<void> resetToDefault(Box<Node> nodeSource) async {
175 final ethereumNodes = await loadDefaultEthereumNodes();
176 final nanoNodes = await loadDefaultNanoNodes();
177 final polygonNodes = await loadDefaultPolygonNodes();
178 + final solanaNodes = await loadDefaultSolanaNodes();
179
180 final nodes = moneroNodes +
181 bitcoinElectrumServerList +
@@ -166,7 +184,8 @@ Future<void> resetToDefault(Box<Node> nodeSource) async {
184 ethereumNodes +
185 bitcoinCashElectrumServerList +
186 nanoNodes +
169 - polygonNodes;
187 + polygonNodes +
188 + solanaNodes;
189
190 await nodeSource.clear();
191 await nodeSource.addAll(nodes);
lib/entities/preferences_key.dart
+1
@@ -13,6 +13,7 @@ class PreferencesKey {
13 static const currentBananoPowNodeIdKey = 'current_node_id_banano_pow';
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 currentTransactionPriorityKeyLegacy = 'current_fee_priority';
18 static const currentBalanceDisplayModeKey = 'current_balance_display_mode';
19 static const shouldSaveRecipientAddressKey = 'save_recipient_address';
lib/entities/priority_for_wallet_type.dart
+4 -3
@@ -21,12 +21,13 @@ List<TransactionPriority> priorityForWalletType(WalletType type) {
21 return ethereum!.getTransactionPriorities();
22 case WalletType.bitcoinCash:
23 return bitcoinCash!.getTransactionPriorities();
24 - // no such thing for nano/banano:
24 + case WalletType.polygon:
25 + return polygon!.getTransactionPriorities();
26 + // no such thing for nano/banano/solana:
27 case WalletType.nano:
28 case WalletType.banano:
29 + case WalletType.solana:
30 return [];
28 - case WalletType.polygon:
29 - return polygon!.getTransactionPriorities();
31 default:
32 return [];
33 }
lib/entities/provider_types.dart
+9
@@ -67,6 +67,8 @@ class ProvidersHelper {
67 return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
68 case WalletType.polygon:
69 return [ProviderType.askEachTime, ProviderType.dfx];
70 + case WalletType.solana:
71 + return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
72 case WalletType.none:
73 case WalletType.haven:
74 return [];
@@ -88,6 +90,13 @@ class ProvidersHelper {
90 return [ProviderType.askEachTime, ProviderType.moonpaySell];
91 case WalletType.polygon:
92 return [ProviderType.askEachTime, ProviderType.dfx];
93 + case WalletType.solana:
94 + return [
95 + ProviderType.askEachTime,
96 + ProviderType.onramper,
97 + ProviderType.robinhood,
98 + ProviderType.moonpaySell,
99 + ];
100 case WalletType.monero:
101 case WalletType.nano:
102 case WalletType.banano:
lib/ethereum/cw_ethereum.dart
+7 -4
@@ -119,12 +119,13 @@ class CWEthereum extends Ethereum {
119 }
120
121 @override
122 - Future<void> addErc20Token(WalletBase wallet, Erc20Token token) async =>
123 - await (wallet as EthereumWallet).addErc20Token(token);
122 + Future<void> addErc20Token(WalletBase wallet, CryptoCurrency token) async {
123 + await (wallet as EthereumWallet).addErc20Token(token as Erc20Token);
124 + }
125
126 @override
126 - Future<void> deleteErc20Token(WalletBase wallet, Erc20Token token) async =>
127 - await (wallet as EthereumWallet).deleteErc20Token(token);
127 + Future<void> deleteErc20Token(WalletBase wallet, CryptoCurrency token) async =>
128 + await (wallet as EthereumWallet).deleteErc20Token(token as Erc20Token);
129
130 @override
131 Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress) async {
@@ -153,4 +154,6 @@ class CWEthereum extends Ethereum {
154 Web3Client? getWeb3Client(WalletBase wallet) {
155 return (wallet as EthereumWallet).getWeb3Client();
156 }
157 +
158 + String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress;
159 }
lib/main.dart
+2 -2
@@ -163,11 +163,11 @@ Future<void> initializeAppConfigs() async {
163 transactionDescriptions: transactionDescriptions,
164 secureStorage: secureStorage,
165 anonpayInvoiceInfo: anonpayInvoiceInfo,
166 - initialMigrationVersion: 26);
166 + initialMigrationVersion: 27);
167 }
168
169 Future<void> initialSetup(
170 - {required SharedPreferences sharedPreferences,
170 + {required SharedPreferences sharedPreferences,
171 required Box<Node> nodes,
172 required Box<Node> powNodes,
173 required Box<WalletInfo> walletInfoSource,
lib/polygon/cw_polygon.dart
+6 -4
@@ -119,12 +119,12 @@ class CWPolygon extends Polygon {
119 }
120
121 @override
122 - Future<void> addErc20Token(WalletBase wallet, Erc20Token token) async =>
123 - await (wallet as PolygonWallet).addErc20Token(token);
122 + Future<void> addErc20Token(WalletBase wallet, CryptoCurrency token) async =>
123 + await (wallet as PolygonWallet).addErc20Token(token as Erc20Token);
124
125 @override
126 - Future<void> deleteErc20Token(WalletBase wallet, Erc20Token token) async =>
127 - await (wallet as PolygonWallet).deleteErc20Token(token);
126 + Future<void> deleteErc20Token(WalletBase wallet, CryptoCurrency token) async =>
127 + await (wallet as PolygonWallet).deleteErc20Token(token as Erc20Token);
128
129 @override
130 Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress) async {
@@ -153,4 +153,6 @@ class CWPolygon extends Polygon {
153 Web3Client? getWeb3Client(WalletBase wallet) {
154 return (wallet as PolygonWallet).getWeb3Client();
155 }
156 +
157 + String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress;
158 }
lib/reactions/fiat_rate_update.dart
+9 -1
@@ -4,9 +4,11 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart';
4 import 'package:cake_wallet/entities/update_haven_rate.dart';
5 import 'package:cake_wallet/ethereum/ethereum.dart';
6 import 'package:cake_wallet/polygon/polygon.dart';
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:cw_core/crypto_currency.dart';
12 import 'package:cw_core/erc20_token.dart';
13 import 'package:cw_core/wallet_type.dart';
14 import 'package:mobx/mobx.dart';
@@ -35,7 +37,7 @@ Future<void> startFiatRateUpdate(
37 torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly);
38 }
39
38 - Iterable<Erc20Token>? currencies;
40 + Iterable<CryptoCurrency>? currencies;
41 if (appStore.wallet!.type == WalletType.ethereum) {
42 currencies =
43 ethereum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled);
@@ -46,6 +48,12 @@ Future<void> startFiatRateUpdate(
48 polygon!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled);
49 }
50
51 + if (appStore.wallet!.type == WalletType.solana) {
52 + currencies =
53 + solana!.getSPLTokenCurrencies(appStore.wallet!).where((element) => element.enabled);
54 + }
55 +
56 +
57 if (currencies != null) {
58 for (final currency in currencies) {
59 () async {
lib/reactions/on_current_wallet_change.dart
+8 -2
@@ -3,6 +3,8 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart';
3 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: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';
@@ -109,7 +111,7 @@ void startCurrentWalletChangeReaction(
111 fiat: settingsStore.fiatCurrency,
112 torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly);
113
112 - Iterable<Erc20Token>? currencies;
114 + Iterable<CryptoCurrency>? currencies;
115 if (wallet.type == WalletType.ethereum) {
116 currencies =
117 ethereum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled);
@@ -118,7 +120,11 @@ void startCurrentWalletChangeReaction(
120 currencies =
121 polygon!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled);
122 }
121 -
123 + if (wallet.type == WalletType.solana) {
124 + currencies =
125 + solana!.getSPLTokenCurrencies(appStore.wallet!).where((element) => element.enabled);
126 + }
127 +
128 if (currencies != null) {
129 for (final currency in currencies) {
130 () async {
lib/reactions/wallet_connect.dart
+18 -1
@@ -1,4 +1,5 @@
1 -import 'package:cake_wallet/core/wallet_connect/evm_chain_id.dart';
1 +import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart';
2 +import 'package:cake_wallet/core/wallet_connect/chain_service/solana/solana_chain_id.dart';
3 import 'package:cw_core/wallet_type.dart';
4
5 bool isEVMCompatibleChain(WalletType walletType) {
@@ -11,12 +12,24 @@ bool isEVMCompatibleChain(WalletType walletType) {
12 }
13 }
14
15 +bool isWalletConnectCompatibleChain(WalletType walletType) {
16 + switch (walletType) {
17 + case WalletType.polygon:
18 + case WalletType.ethereum:
19 + return true;
20 + default:
21 + return false;
22 + }
23 +}
24 +
25 String getChainNameSpaceAndIdBasedOnWalletType(WalletType walletType) {
26 switch (walletType) {
27 case WalletType.ethereum:
28 return EVMChainId.ethereum.chain();
29 case WalletType.polygon:
30 return EVMChainId.polygon.chain();
31 + case WalletType.solana:
32 + return SolanaChainId.mainnet.chain();
33 default:
34 return '';
35 }
@@ -40,6 +53,8 @@ String getChainNameBasedOnWalletType(WalletType walletType) {
53 return 'eth';
54 case WalletType.polygon:
55 return 'polygon';
56 + case WalletType.solana:
57 + return 'solana';
58 default:
59 return '';
60 }
@@ -51,6 +66,8 @@ String getTokenNameBasedOnWalletType(WalletType walletType) {
66 return 'ETH';
67 case WalletType.polygon:
68 return 'MATIC';
69 + case WalletType.solana:
70 + return 'SOL';
71 default:
72 return '';
73 }
lib/solana/cw_solana.dart new
+118
@@ -0,0 +1,118 @@
1 +part of 'solana.dart';
2 +
3 +class CWSolana extends Solana {
4 + @override
5 + List<String> getSolanaWordList(String language) => SolanaMnemonics.englishWordlist;
6 +
7 + WalletService createSolanaWalletService(Box<WalletInfo> walletInfoSource) =>
8 + SolanaWalletService(walletInfoSource);
9 +
10 + @override
11 + WalletCredentials createSolanaNewWalletCredentials({
12 + required String name,
13 + WalletInfo? walletInfo,
14 + }) =>
15 + SolanaNewWalletCredentials(name: name, walletInfo: walletInfo);
16 +
17 + @override
18 + WalletCredentials createSolanaRestoreWalletFromSeedCredentials({
19 + required String name,
20 + required String mnemonic,
21 + required String password,
22 + }) =>
23 + SolanaRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic);
24 +
25 + @override
26 + WalletCredentials createSolanaRestoreWalletFromPrivateKey({
27 + required String name,
28 + required String privateKey,
29 + required String password,
30 + }) =>
31 + SolanaRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey);
32 +
33 + @override
34 + String getAddress(WalletBase wallet) => (wallet as SolanaWallet).walletAddresses.address;
35 +
36 + @override
37 + String getPrivateKey(WalletBase wallet) => (wallet as SolanaWallet).privateKey;
38 +
39 + @override
40 + String getPublicKey(WalletBase wallet) => (wallet as SolanaWallet).keys.publicKey.toBase58();
41 +
42 + @override
43 + Ed25519HDKeyPair? getWalletKeyPair(WalletBase wallet) => (wallet as SolanaWallet).walletKeyPair;
44 +
45 + Object createSolanaTransactionCredentials(
46 + List<Output> outputs, {
47 + required CryptoCurrency currency,
48 + }) =>
49 + SolanaTransactionCredentials(
50 + outputs
51 + .map((out) => OutputInfo(
52 + fiatAmount: out.fiatAmount,
53 + cryptoAmount: out.cryptoAmount,
54 + address: out.address,
55 + note: out.note,
56 + sendAll: out.sendAll,
57 + extractedAddress: out.extractedAddress,
58 + isParsedAddress: out.isParsedAddress,
59 + formattedCryptoAmount: out.formattedCryptoAmount))
60 + .toList(),
61 + currency: currency,
62 + );
63 +
64 + Object createSolanaTransactionCredentialsRaw(
65 + List<OutputInfo> outputs, {
66 + required CryptoCurrency currency,
67 + }) =>
68 + SolanaTransactionCredentials(outputs, currency: currency);
69 +
70 + @override
71 + List<SPLToken> getSPLTokenCurrencies(WalletBase wallet) {
72 + final solanaWallet = wallet as SolanaWallet;
73 + return solanaWallet.splTokenCurrencies;
74 + }
75 +
76 + @override
77 + Future<void> addSPLToken(WalletBase wallet, CryptoCurrency token) async =>
78 + await (wallet as SolanaWallet).addSPLToken(token as SPLToken);
79 +
80 + @override
81 + Future<void> deleteSPLToken(WalletBase wallet, CryptoCurrency token) async =>
82 + await (wallet as SolanaWallet).deleteSPLToken(token as SPLToken);
83 +
84 + @override
85 + Future<SPLToken?> getSPLToken(WalletBase wallet, String mintAddress) async {
86 + final solanaWallet = wallet as SolanaWallet;
87 + return await solanaWallet.getSPLToken(mintAddress);
88 + }
89 +
90 + @override
91 + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) {
92 + transaction as SolanaTransactionInfo;
93 + if (transaction.tokenSymbol == CryptoCurrency.sol.title) {
94 + return CryptoCurrency.sol;
95 + }
96 +
97 + wallet as SolanaWallet;
98 + return wallet.splTokenCurrencies
99 + .firstWhere((element) => transaction.tokenSymbol == element.symbol);
100 + }
101 +
102 + @override
103 + double getTransactionAmountRaw(TransactionInfo transactionInfo) {
104 + return (transactionInfo as SolanaTransactionInfo).solAmount.toDouble();
105 + }
106 +
107 + @override
108 + String getTokenAddress(CryptoCurrency asset) => (asset as SPLToken).mintAddress;
109 +
110 + @override
111 + List<int>? getValidationLength(CryptoCurrency type) {
112 + if (type is SPLToken) {
113 + return [44];
114 + }
115 +
116 + return null;
117 + }
118 +}
lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart
+3
@@ -37,6 +37,7 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
37 final bitcoinCashIcon = Image.asset('assets/images/bch_icon.png', height: 24, width: 24);
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 nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24);
42
43 Image _newWalletImage(BuildContext context) => Image.asset(
@@ -153,6 +154,8 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
154 return bananoIcon;
155 case WalletType.polygon:
156 return polygonIcon;
157 + case WalletType.solana:
158 + return solanaIcon;
159 default:
160 return nonWalletTypeIcon;
161 }
lib/src/screens/dashboard/edit_token_page.dart
+27 -21
@@ -8,6 +8,7 @@ import 'package:cake_wallet/src/widgets/primary_button.dart';
8 import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
9 import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
10 import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart';
11 +import 'package:cw_core/crypto_currency.dart';
12 import 'package:cw_core/erc20_token.dart';
13 import 'package:flutter/material.dart';
14 import 'package:flutter/services.dart';
@@ -16,12 +17,12 @@ class EditTokenPage extends BasePage {
17 EditTokenPage({
18 Key? key,
19 required this.homeSettingsViewModel,
19 - this.erc20token,
20 + this.token,
21 this.initialContractAddress,
21 - }) : assert(erc20token == null || initialContractAddress == null);
22 + }) : assert(token == null || initialContractAddress == null);
23
24 final HomeSettingsViewModel homeSettingsViewModel;
24 - final Erc20Token? erc20token;
25 + final CryptoCurrency? token;
26 final String? initialContractAddress;
27
28 @override
@@ -31,7 +32,7 @@ class EditTokenPage extends BasePage {
32 Widget body(BuildContext context) {
33 return EditTokenPageBody(
34 homeSettingsViewModel: homeSettingsViewModel,
34 - erc20token: erc20token,
35 + token: token,
36 initialContractAddress: initialContractAddress,
37 );
38 }
@@ -41,12 +42,12 @@ class EditTokenPageBody extends StatefulWidget {
42 const EditTokenPageBody({
43 Key? key,
44 required this.homeSettingsViewModel,
44 - this.erc20token,
45 + this.token,
46 this.initialContractAddress,
47 }) : super(key: key);
48
49 final HomeSettingsViewModel homeSettingsViewModel;
49 - final Erc20Token? erc20token;
50 + final CryptoCurrency? token;
51 final String? initialContractAddress;
52
53 @override
@@ -73,11 +74,15 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
74 void initState() {
75 super.initState();
76
76 - if (widget.erc20token != null) {
77 - _contractAddressController.text = widget.erc20token!.contractAddress;
78 - _tokenNameController.text = widget.erc20token!.name;
79 - _tokenSymbolController.text = widget.erc20token!.symbol;
80 - _tokenDecimalController.text = widget.erc20token!.decimal.toString();
77 + String? address;
78 +
79 + if (widget.token != null) {
80 + address = widget.homeSettingsViewModel.getTokenAddressBasedOnWallet(widget.token!);
81 +
82 + _contractAddressController.text = address ?? '';
83 + _tokenNameController.text = widget.token!.name;
84 + _tokenSymbolController.text = widget.token!.title;
85 + _tokenDecimalController.text = widget.token!.decimals.toString();
86 }
87
88 if (widget.initialContractAddress != null) {
@@ -91,7 +96,7 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
96 }
97
98 final contractAddress = _contractAddressController.text;
94 - if (contractAddress.isNotEmpty && contractAddress != widget.erc20token?.contractAddress) {
99 + if (contractAddress.isNotEmpty && contractAddress != address) {
100 setState(() {
101 _showDisclaimer = true;
102 });
@@ -139,7 +144,9 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
144 style: TextStyle(
145 fontSize: 14,
146 fontWeight: FontWeight.normal,
142 - color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
147 + color: Theme.of(context)
148 + .extension<TransactionTradeTheme>()!
149 + .detailsTitlesColor,
150 ),
151 ),
152 ),
@@ -172,12 +179,12 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
179 Expanded(
180 child: PrimaryButton(
181 onPressed: () async {
175 - if (widget.erc20token != null) {
176 - await widget.homeSettingsViewModel.deleteErc20Token(widget.erc20token!);
182 + if (widget.token != null) {
183 + await widget.homeSettingsViewModel.deleteToken(widget.token!);
184 }
185 Navigator.pop(context);
186 },
180 - text: widget.erc20token != null ? S.of(context).delete : S.of(context).cancel,
187 + text: widget.token != null ? S.of(context).delete : S.of(context).cancel,
188 color: Colors.red,
189 textColor: Colors.white,
190 ),
@@ -188,7 +195,7 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
195 onPressed: () async {
196 if (_formKey.currentState!.validate() &&
197 (!_showDisclaimer || _disclaimerChecked)) {
191 - await widget.homeSettingsViewModel.addErc20Token(Erc20Token(
198 + await widget.homeSettingsViewModel.addToken(Erc20Token(
199 name: _tokenNameController.text,
200 symbol: _tokenSymbolController.text,
201 contractAddress: _contractAddressController.text,
@@ -214,14 +221,13 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
221
222 void _getTokenInfo() async {
223 if (_contractAddressController.text.isNotEmpty) {
217 - final token =
218 - await widget.homeSettingsViewModel.getErc20Token(_contractAddressController.text);
224 + final token = await widget.homeSettingsViewModel.getToken(_contractAddressController.text);
225
226 if (token != null) {
227 if (_tokenNameController.text.isEmpty) _tokenNameController.text = token.name;
222 - if (_tokenSymbolController.text.isEmpty) _tokenSymbolController.text = token.symbol;
228 + if (_tokenSymbolController.text.isEmpty) _tokenSymbolController.text = token.title;
229 if (_tokenDecimalController.text.isEmpty)
224 - _tokenDecimalController.text = token.decimal.toString();
230 + _tokenDecimalController.text = token.decimals.toString();
231 }
232 }
233 }
lib/src/screens/dashboard/home_settings_page.dart
+10 -12
@@ -5,6 +5,7 @@ import 'package:cake_wallet/entities/sort_balance_types.dart';
5 import 'package:cake_wallet/generated/i18n.dart';
6 import 'package:cake_wallet/routes.dart';
7 import 'package:cake_wallet/src/screens/base_page.dart';
8 +import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
9 import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart';
10 import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
11 import 'package:cake_wallet/themes/extensions/address_theme.dart';
@@ -117,7 +118,7 @@ class HomeSettingsPage extends BasePage {
118
119 return SettingsSwitcherCell(
120 title: "${token.name} "
120 - "(${token.symbol})",
121 + "(${token.title})",
122 value: token.enabled,
123 onValueChange: (_, bool value) {
124 _homeSettingsViewModel.changeTokenAvailability(token, value);
@@ -128,20 +129,16 @@ class HomeSettingsPage extends BasePage {
129 'token': token,
130 });
131 },
131 - leading: token.iconPath != null
132 - ? Container(
133 - child: Image.asset(
134 - token.iconPath!,
135 - height: 30.0,
136 - width: 30.0,
137 - ),
138 - )
139 - : Container(
132 + leading: CakeImageWidget(
133 + imageUrl: token.iconPath,
134 + height: 40,
135 + width: 40,
136 + displayOnError: Container(
137 height: 30.0,
138 width: 30.0,
139 child: Center(
140 child: Text(
144 - token.symbol.substring(0, min(token.symbol.length, 2)),
141 + token.title.substring(0, min(token.title.length, 2)),
142 style: TextStyle(fontSize: 11),
143 ),
144 ),
@@ -149,7 +146,8 @@ class HomeSettingsPage extends BasePage {
146 shape: BoxShape.circle,
147 color: Colors.grey.shade400,
148 ),
152 - ),
149 + ),
150 + ),
151 decoration: BoxDecoration(
152 color: Theme.of(context).cardColor,
153 borderRadius: BorderRadius.circular(30),
lib/src/screens/dashboard/pages/balance_page.dart
+7 -9
@@ -6,6 +6,7 @@ import 'package:cake_wallet/reactions/wallet_connect.dart';
6 import 'package:cake_wallet/routes.dart';
7 import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart';
8 import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart';
9 +import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
10 import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart';
11 import 'package:cake_wallet/src/widgets/introducing_card.dart';
12 import 'package:cake_wallet/store/settings_store.dart';
@@ -333,15 +334,11 @@ class BalanceRowWidget extends StatelessWidget {
334 child: Center(
335 child: Column(
336 children: [
336 - currency.iconPath != null
337 - ? Container(
338 - child: Image.asset(
339 - currency.iconPath!,
340 - height: 40.0,
341 - width: 40.0,
342 - ),
343 - )
344 - : Container(
337 + CakeImageWidget(
338 + imageUrl: currency.iconPath,
339 + height: 40,
340 + width: 40,
341 + displayOnError: Container(
342 height: 30.0,
343 width: 30.0,
344 child: Center(
@@ -355,6 +352,7 @@ class BalanceRowWidget extends StatelessWidget {
352 color: Colors.grey.shade400,
353 ),
354 ),
355 + ),
356 const SizedBox(height: 10),
357 Text(
358 currency.title,
lib/src/screens/dashboard/pages/nft_details_page.dart
+2 -2
@@ -2,7 +2,7 @@ import 'package:cake_wallet/entities/wallet_nft_response.dart';
2 import 'package:cake_wallet/generated/i18n.dart';
3 import 'package:cake_wallet/src/screens/base_page.dart';
4 import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart';
5 -import 'package:cake_wallet/src/screens/dashboard/widgets/nft_image_tile_widget.dart';
5 +import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
6 import 'package:cake_wallet/src/widgets/gradient_background.dart';
7 import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
8 import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
@@ -94,7 +94,7 @@ class NFTDetailsPage extends BasePage {
94 .syncedBackgroundColor,
95
96 ),
97 - child: NFTImageWidget(
97 + child: CakeImageWidget(
98 imageUrl: nftAsset.normalizedMetadata?.imageUrl,
99 ),
100 ),
lib/src/screens/dashboard/widgets/menu_widget.dart
+19 -17
@@ -18,23 +18,23 @@ class MenuWidget extends StatefulWidget {
18
19 class MenuWidgetState extends State<MenuWidget> {
20 MenuWidgetState()
21 - : this.menuWidth = 0,
22 - this.screenWidth = 0,
23 - this.screenHeight = 0,
24 - this.headerHeight = 120,
25 - this.tileHeight = 60,
26 - this.fromTopEdge = 50,
27 - this.fromBottomEdge = 25,
28 - this.moneroIcon = Image.asset('assets/images/monero_menu.png'),
29 - this.bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png'),
30 - this.litecoinIcon = Image.asset('assets/images/litecoin_menu.png'),
31 - this.havenIcon = Image.asset('assets/images/haven_menu.png'),
32 - this.ethereumIcon = Image.asset('assets/images/eth_icon.png'),
33 - this.nanoIcon = Image.asset('assets/images/nano_icon.png'),
34 - this.bananoIcon = Image.asset('assets/images/nano_icon.png'),
21 + : this.menuWidth = 0,
22 + this.screenWidth = 0,
23 + this.screenHeight = 0,
24 + this.headerHeight = 120,
25 + this.tileHeight = 60,
26 + this.fromTopEdge = 50,
27 + this.fromBottomEdge = 25,
28 + this.moneroIcon = Image.asset('assets/images/monero_menu.png'),
29 + this.bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png'),
30 + this.litecoinIcon = Image.asset('assets/images/litecoin_menu.png'),
31 + this.havenIcon = Image.asset('assets/images/haven_menu.png'),
32 + this.ethereumIcon = Image.asset('assets/images/eth_icon.png'),
33 + this.nanoIcon = Image.asset('assets/images/nano_icon.png'),
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 -
36 + this.polygonIcon = Image.asset('assets/images/matic_icon.png'),
37 + this.solanaIcon = Image.asset('assets/images/sol_icon.png');
38
39 final largeScreen = 731;
40
@@ -56,7 +56,7 @@ class MenuWidgetState extends State<MenuWidget> {
56 Image nanoIcon;
57 Image bananoIcon;
58 Image polygonIcon;
59 -
59 + Image solanaIcon;
60
61 @override
62 void initState() {
@@ -224,6 +224,8 @@ class MenuWidgetState extends State<MenuWidget> {
224 return bananoIcon;
225 case WalletType.polygon:
226 return polygonIcon;
227 + case WalletType.solana:
228 + return solanaIcon;
229 default:
230 throw Exception('No icon for ${type.toString()}');
231 }
lib/src/screens/dashboard/widgets/nft_tile_widget.dart
+2 -3
@@ -1,9 +1,8 @@
1 import 'package:cake_wallet/entities/wallet_nft_response.dart';
2 import 'package:cake_wallet/routes.dart';
3 -import 'package:cake_wallet/src/screens/dashboard/widgets/nft_image_tile_widget.dart';
3 +import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
4 import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
5 import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart';
6 -import 'package:flutter/cupertino.dart';
6 import 'package:flutter/material.dart';
7
8 class NFTTileWidget extends StatelessWidget {
@@ -38,7 +37,7 @@ class NFTTileWidget extends StatelessWidget {
37 ),
38 color: Theme.of(context).extension<SyncIndicatorTheme>()!.syncedBackgroundColor,
39 ),
41 - child: NFTImageWidget(
40 + child: CakeImageWidget(
41 imageUrl: nftAsset.normalizedMetadata?.imageUrl,
42 ),
43 ),
lib/src/screens/send/send_page.dart
+10 -2
@@ -14,6 +14,7 @@ import 'package:cake_wallet/utils/payment_request.dart';
14 import 'package:cake_wallet/utils/request_review_handler.dart';
15 import 'package:cake_wallet/utils/responsive_layout_util.dart';
16 import 'package:cake_wallet/view_model/send/output.dart';
17 +import 'package:cw_core/wallet_type.dart';
18 import 'package:flutter/material.dart';
19 import 'package:flutter_mobx/flutter_mobx.dart';
20 import 'package:mobx/mobx.dart';
@@ -439,10 +440,17 @@ class SendPage extends BasePage {
440 }
441
442 if (state is TransactionCommitted) {
443 + String alertContent;
444 + if (sendViewModel.walletType == WalletType.solana) {
445 + alertContent =
446 + '${S.of(_dialogContext).send_success(sendViewModel.selectedCryptoCurrency.toString())}. ${S.of(_dialogContext).waitFewSecondForTxUpdate}';
447 + } else {
448 + alertContent = S.of(_dialogContext).send_success(
449 + sendViewModel.selectedCryptoCurrency.toString());
450 + }
451 return AlertWithOneAction(
452 alertTitle: '',
444 - alertContent: S.of(_dialogContext).send_success(
445 - sendViewModel.selectedCryptoCurrency.toString()),
453 + alertContent: alertContent,
454 buttonText: S.of(_dialogContext).ok,
455 buttonAction: () {
456 Navigator.of(_dialogContext).pop();
lib/src/screens/send/widgets/send_card.dart
+1 -1
@@ -321,7 +321,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
321 ? sendViewModel.allAmountValidator
322 : sendViewModel.amountValidator,
323 ),
324 - if (!sendViewModel.isBatchSending)
324 + if (!sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL)
325 Positioned(
326 top: 2,
327 right: 0,
lib/src/screens/settings/connection_sync_page.dart
+1 -1
@@ -107,7 +107,7 @@ class ConnectionSyncPage extends BasePage {
107 );
108 },
109 ),
110 - if (isEVMCompatibleChain(dashboardViewModel.wallet.type)) ...[
110 + if (isWalletConnectCompatibleChain(dashboardViewModel.wallet.type)) ...[
111 WalletConnectTile(
112 onTap: () => Navigator.of(context).pushNamed(Routes.walletConnectConnectionsListing),
113 ),
lib/src/screens/settings/other_settings_page.dart
+1 -1
@@ -26,7 +26,7 @@ class OtherSettingsPage extends BasePage {
26 padding: EdgeInsets.only(top: 10),
27 child: Column(
28 children: [
29 - if (!_otherSettingsViewModel.changeRepresentativeEnabled)
29 + if (_otherSettingsViewModel.displayTransactionPriority)
30 SettingsPickerCell(
31 title: S.current.settings_fee_priority,
32 items: priorityForWalletType(_otherSettingsViewModel.walletType),
lib/src/screens/wallet_connect/widgets/pairing_item_widget.dart
+6 -6
@@ -1,4 +1,5 @@
1 import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
3 import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
4 import 'package:cake_wallet/themes/extensions/receive_page_theme.dart';
5 import 'package:flutter/material.dart';
@@ -26,12 +27,11 @@ class PairingItemWidget extends StatelessWidget {
27 '$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
28
29 return ListTile(
29 - leading: CircleAvatar(
30 - backgroundImage: (metadata.icons.isNotEmpty
31 - ? NetworkImage(metadata.icons[0])
32 - : const AssetImage(
33 - 'assets/images/default_icon.png',
34 - )) as ImageProvider<Object>,
30 + leading: CakeImageWidget(
31 + imageUrl: metadata.icons.isNotEmpty ? metadata.icons[0]: null,
32 + displayOnError: CircleAvatar(
33 + backgroundImage: AssetImage('assets/images/default_icon.png'),
34 + ),
35 ),
36 title: Text(
37 metadata.name,
lib/src/screens/wallet_list/wallet_list_page.dart
+3
@@ -103,6 +103,7 @@ class WalletListBodyState extends State<WalletListBody> {
103 final bitcoinCashIcon = Image.asset('assets/images/bch_icon.png', height: 24, width: 24);
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 scrollController = ScrollController();
108 final double tileHeight = 60;
109 Flushbar<void>? _progressBar;
@@ -313,6 +314,8 @@ class WalletListBodyState extends State<WalletListBody> {
314 return nanoIcon;
315 case WalletType.polygon:
316 return polygonIcon;
317 + case WalletType.solana:
318 + return solanaIcon;
319 default:
320 return nonWalletTypeIcon;
321 }
lib/src/widgets/cake_image_widget.dart renamed
+26 -6
@@ -2,25 +2,45 @@ import 'package:flutter/cupertino.dart';
2 import 'package:flutter/material.dart';
3 import 'package:flutter_svg/flutter_svg.dart';
4
5 -class NFTImageWidget extends StatelessWidget {
6 - const NFTImageWidget({
5 +class CakeImageWidget extends StatelessWidget {
6 + CakeImageWidget({
7 required this.imageUrl,
8 - });
8 + Widget? displayOnError,
9 + this.height,
10 + this.width,
11 + }) : _displayOnError = displayOnError ?? Icon(Icons.error);
12
13 final String? imageUrl;
14 + final double? height;
15 + final double? width;
16 + final Widget? _displayOnError;
17
18 @override
19 Widget build(BuildContext context) {
20 try {
15 - if (imageUrl == null) return Icon(Icons.error);
21 + if (imageUrl == null) return _displayOnError!;
22 +
23 + if (imageUrl!.contains('assets/images')) {
24 + return Image.asset(
25 + imageUrl!,
26 + height: height,
27 + width: width,
28 + );
29 + }
30
31 if (imageUrl!.contains('.svg')) {
18 - return SvgPicture.network(imageUrl!);
32 + return SvgPicture.network(
33 + imageUrl!,
34 + height: height,
35 + width: width,
36 + );
37 }
38
39 return Image.network(
40 imageUrl!,
41 fit: BoxFit.cover,
42 + height: height,
43 + width: width,
44 loadingBuilder: (BuildContext _, Widget child, ImageChunkEvent? loadingProgress) {
45 if (loadingProgress == null) {
46 return child;
@@ -31,7 +51,7 @@ class NFTImageWidget extends StatelessWidget {
51 errorBuilder: (_, __, ___) => Icon(Icons.error),
52 );
53 } catch (_) {
34 - return Icon(Icons.error);
54 + return _displayOnError!;
55 }
56 }
57 }
lib/store/app_store.dart
+1 -1
@@ -41,7 +41,7 @@ abstract class AppStoreBase with Store {
41 this.wallet = wallet;
42 this.wallet!.setExceptionHandler(ExceptionHandler.onError);
43
44 - if (isEVMCompatibleChain(wallet.type)) {
44 + if (isWalletConnectCompatibleChain(wallet.type)) {
45 await getIt.get<Web3WalletService>().onDispose();
46 getIt.get<Web3WalletService>().create();
47 await getIt.get<Web3WalletService>().init();
lib/store/settings_store.dart
+15 -1
@@ -838,6 +838,7 @@ abstract class SettingsStoreBase with Store {
838 final polygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey);
839 final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey);
840 final nanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey);
841 + final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey);
842 final moneroNode = nodeSource.get(nodeId);
843 final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId);
844 final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId);
@@ -847,6 +848,7 @@ abstract class SettingsStoreBase with Store {
848 final bitcoinCashElectrumServer = nodeSource.get(bitcoinCashElectrumServerId);
849 final nanoNode = nodeSource.get(nanoNodeId);
850 final nanoPowNode = powNodeSource.get(nanoPowNodeId);
851 + final solanaNode = nodeSource.get(solanaNodeId);
852 final packageInfo = await PackageInfo.fromPlatform();
853 final deviceName = await _getDeviceName() ?? '';
854 final shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true;
@@ -903,6 +905,10 @@ abstract class SettingsStoreBase with Store {
905 powNodes[WalletType.nano] = nanoPowNode;
906 }
907
908 + if (solanaNode != null) {
909 + nodes[WalletType.solana] = solanaNode;
910 + }
911 +
912 final savedSyncMode = SyncMode.all.firstWhere((element) {
913 return element.type.index == (sharedPreferences.getInt(PreferencesKey.syncModeKey) ?? 0);
914 });
@@ -1190,6 +1196,7 @@ abstract class SettingsStoreBase with Store {
1196 final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey);
1197 final polygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey);
1198 final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey);
1199 + final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey);
1200 final moneroNode = nodeSource.get(nodeId);
1201 final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId);
1202 final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId);
@@ -1198,7 +1205,7 @@ abstract class SettingsStoreBase with Store {
1205 final polygonNode = nodeSource.get(polygonNodeId);
1206 final bitcoinCashNode = nodeSource.get(bitcoinCashElectrumServerId);
1207 final nanoNode = nodeSource.get(nanoNodeId);
1201 -
1208 + final solanaNode = nodeSource.get(solanaNodeId);
1209 if (moneroNode != null) {
1210 nodes[WalletType.monero] = moneroNode;
1211 }
@@ -1231,6 +1238,10 @@ abstract class SettingsStoreBase with Store {
1238 nodes[WalletType.nano] = nanoNode;
1239 }
1240
1241 + if (solanaNode != null) {
1242 + nodes[WalletType.solana] = solanaNode;
1243 + }
1244 +
1245 // MIGRATED:
1246
1247 useTOTP2FA = await SecureKey.getBool(
@@ -1358,6 +1369,9 @@ abstract class SettingsStoreBase with Store {
1369 case WalletType.polygon:
1370 await _sharedPreferences.setInt(PreferencesKey.currentPolygonNodeIdKey, node.key as int);
1371 break;
1372 + case WalletType.solana:
1373 + await _sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, node.key as int);
1374 + break;
1375 default:
1376 break;
1377 }
lib/view_model/advanced_privacy_settings_view_model.dart
+4 -1
@@ -28,7 +28,10 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store {
28 final SettingsStore _settingsStore;
29
30 bool get hasSeedPhraseLengthOption =>
31 - type == WalletType.bitcoinCash || type == WalletType.ethereum;
31 + type == WalletType.bitcoinCash ||
32 + type == WalletType.ethereum ||
33 + type == WalletType.polygon ||
34 + type == WalletType.solana;
35
36 bool get hasSeedTypeOption => type == WalletType.monero;
37
lib/view_model/dashboard/balance_view_model.dart
+64 -56
@@ -18,15 +18,15 @@ import 'package:mobx/mobx.dart';
18 part 'balance_view_model.g.dart';
19
20 class BalanceRecord {
21 - const BalanceRecord({
22 - required this.availableBalance,
23 - required this.additionalBalance,
24 - required this.frozenBalance,
25 - required this.fiatAvailableBalance,
26 - required this.fiatAdditionalBalance,
27 - required this.fiatFrozenBalance,
28 - required this.asset,
29 - required this.formattedAssetTitle});
21 + const BalanceRecord(
22 + {required this.availableBalance,
23 + required this.additionalBalance,
24 + required this.frozenBalance,
25 + required this.fiatAvailableBalance,
26 + required this.fiatAdditionalBalance,
27 + required this.fiatFrozenBalance,
28 + required this.asset,
29 + required this.formattedAssetTitle});
30 final String fiatAdditionalBalance;
31 final String fiatAvailableBalance;
32 final String fiatFrozenBalance;
@@ -41,12 +41,10 @@ class BalanceViewModel = BalanceViewModelBase with _$BalanceViewModel;
41
42 abstract class BalanceViewModelBase with Store {
43 BalanceViewModelBase(
44 - {required this.appStore,
45 - required this.settingsStore,
46 - required this.fiatConvertationStore})
47 - : isReversing = false,
48 - isShowCard = appStore.wallet!.walletInfo.isShowIntroCakePayCard,
49 - wallet = appStore.wallet! {
44 + {required this.appStore, required this.settingsStore, required this.fiatConvertationStore})
45 + : isReversing = false,
46 + isShowCard = appStore.wallet!.walletInfo.isShowIntroCakePayCard,
47 + wallet = appStore.wallet! {
48 reaction((_) => appStore.wallet, _onWalletChange);
49 }
50
@@ -60,8 +58,7 @@ abstract class BalanceViewModelBase with Store {
58 bool isReversing;
59
60 @observable
63 - WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>
64 - wallet;
61 + WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
62
63 @computed
64 double get price {
@@ -82,7 +79,8 @@ abstract class BalanceViewModelBase with Store {
79 bool get isFiatDisabled => settingsStore.fiatApiMode == FiatApiMode.disabled;
80
81 @computed
85 - bool get isHomeScreenSettingsEnabled => isEVMCompatibleChain(wallet.type);
82 + bool get isHomeScreenSettingsEnabled =>
83 + isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana;
84
85 @computed
86 bool get hasAccounts => wallet.type == WalletType.monero;
@@ -97,7 +95,7 @@ abstract class BalanceViewModelBase with Store {
95 String get asset {
96 final typeFormatted = walletTypeToString(appStore.wallet!.type);
97
100 - switch(wallet.type) {
98 + switch (wallet.type) {
99 case WalletType.haven:
100 return '$typeFormatted Assets';
101 default:
@@ -120,13 +118,14 @@ abstract class BalanceViewModelBase with Store {
118
119 @computed
120 String get availableBalanceLabel {
123 - switch(wallet.type) {
121 + switch (wallet.type) {
122 case WalletType.monero:
123 case WalletType.haven:
124 case WalletType.ethereum:
125 case WalletType.polygon:
126 case WalletType.nano:
127 case WalletType.banano:
128 + case WalletType.solana:
129 return S.current.xmr_available_balance;
130 default:
131 return S.current.confirmed;
@@ -135,11 +134,12 @@ abstract class BalanceViewModelBase with Store {
134
135 @computed
136 String get additionalBalanceLabel {
138 - switch(wallet.type) {
137 + switch (wallet.type) {
138 case WalletType.monero:
139 case WalletType.haven:
140 case WalletType.ethereum:
141 case WalletType.polygon:
142 + case WalletType.solana:
143 return S.current.xmr_full_balance;
144 case WalletType.nano:
145 case WalletType.banano:
@@ -228,15 +228,17 @@ abstract class BalanceViewModelBase with Store {
228 Map<CryptoCurrency, BalanceRecord> get balances {
229 return wallet.balance.map((key, value) {
230 if (displayMode == BalanceDisplayMode.hiddenBalance) {
231 - return MapEntry(key, BalanceRecord(
232 - availableBalance: '---',
233 - additionalBalance: '---',
234 - frozenBalance: '---',
235 - fiatAdditionalBalance: isFiatDisabled ? '' : '---',
236 - fiatAvailableBalance: isFiatDisabled ? '' : '---',
237 - fiatFrozenBalance: isFiatDisabled ? '' : '---',
238 - asset: key,
239 - formattedAssetTitle: _formatterAsset(key)));
231 + return MapEntry(
232 + key,
233 + BalanceRecord(
234 + availableBalance: '---',
235 + additionalBalance: '---',
236 + frozenBalance: '---',
237 + fiatAdditionalBalance: isFiatDisabled ? '' : '---',
238 + fiatAvailableBalance: isFiatDisabled ? '' : '---',
239 + fiatFrozenBalance: isFiatDisabled ? '' : '---',
240 + asset: key,
241 + formattedAssetTitle: _formatterAsset(key)));
242 }
243 final fiatCurrency = settingsStore.fiatCurrency;
244 final price = fiatConvertationStore.prices[key] ?? 0;
@@ -245,25 +247,23 @@ abstract class BalanceViewModelBase with Store {
247 // throw Exception('Price is null for: $key');
248 // }
249
248 - final additionalFiatBalance = isFiatDisabled ? '' : (fiatCurrency.toString()
249 - + ' '
250 - + _getFiatBalance(
251 - price: price,
252 - cryptoAmount: value.formattedAdditionalBalance));
253 -
254 - final availableFiatBalance = isFiatDisabled ? '' : (fiatCurrency.toString()
255 - + ' '
256 - + _getFiatBalance(
257 - price: price,
258 - cryptoAmount: value.formattedAvailableBalance));
250 + final additionalFiatBalance = isFiatDisabled
251 + ? ''
252 + : (fiatCurrency.toString() +
253 + ' ' +
254 + _getFiatBalance(price: price, cryptoAmount: value.formattedAdditionalBalance));
255
256 + final availableFiatBalance = isFiatDisabled
257 + ? ''
258 + : (fiatCurrency.toString() +
259 + ' ' +
260 + _getFiatBalance(price: price, cryptoAmount: value.formattedAvailableBalance));
261
261 - final frozenFiatBalance = isFiatDisabled ? '' : (fiatCurrency.toString()
262 - + ' '
263 - + _getFiatBalance(
264 - price: price,
265 - cryptoAmount: getFormattedFrozenBalance(value)));
266 -
262 + final frozenFiatBalance = isFiatDisabled
263 + ? ''
264 + : (fiatCurrency.toString() +
265 + ' ' +
266 + _getFiatBalance(price: price, cryptoAmount: getFormattedFrozenBalance(value)));
267
268 return MapEntry(
269 key,
@@ -276,12 +276,22 @@ abstract class BalanceViewModelBase with Store {
276 fiatFrozenBalance: frozenFiatBalance,
277 asset: key,
278 formattedAssetTitle: _formatterAsset(key)));
279 - });
279 + });
280 }
281
282 @computed
283 - bool get hasAdditionalBalance => !isEVMCompatibleChain(wallet.type);
284 -
283 + bool get hasAdditionalBalance => _hasAdditionBalanceForWalletType(wallet.type);
284 +
285 + bool _hasAdditionBalanceForWalletType(WalletType type) {
286 + switch (type) {
287 + case WalletType.ethereum:
288 + case WalletType.polygon:
289 + case WalletType.solana:
290 + return false;
291 + default:
292 + return true;
293 + }
294 + }
295
296 @computed
297 List<BalanceRecord> get formattedBalances {
@@ -358,9 +368,7 @@ abstract class BalanceViewModelBase with Store {
368
369 @action
370 void _onWalletChange(
361 - WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
362 - TransactionInfo>?
363 - wallet) {
371 + WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>? wallet) {
372 if (wallet == null) {
373 return;
374 }
@@ -371,7 +379,7 @@ abstract class BalanceViewModelBase with Store {
379 }
380
381 @action
374 - Future<void> disableIntroCakePayCard () async {
382 + Future<void> disableIntroCakePayCard() async {
383 const cardDisplayStatus = false;
384 wallet.walletInfo.showIntroCakePayCard = cardDisplayStatus;
385 await wallet.walletInfo.save();
@@ -401,6 +409,6 @@ abstract class BalanceViewModelBase with Store {
409 }
410 }
411
404 - String getFormattedFrozenBalance(Balance walletBalance) => walletBalance.formattedUnAvailableBalance;
412 + String getFormattedFrozenBalance(Balance walletBalance) =>
413 + walletBalance.formattedUnAvailableBalance;
414 }
406 -
lib/view_model/dashboard/home_settings_view_model.dart
+64 -14
@@ -3,6 +3,7 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart';
3 import 'package:cake_wallet/entities/sort_balance_types.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/store/settings_store.dart';
8 import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
9 import 'package:cw_core/crypto_currency.dart';
@@ -16,14 +17,14 @@ class HomeSettingsViewModel = HomeSettingsViewModelBase with _$HomeSettingsViewM
17
18 abstract class HomeSettingsViewModelBase with Store {
19 HomeSettingsViewModelBase(this._settingsStore, this._balanceViewModel)
19 - : tokens = ObservableSet<Erc20Token>() {
20 + : tokens = ObservableSet<CryptoCurrency>() {
21 _updateTokensList();
22 }
23
24 final SettingsStore _settingsStore;
25 final BalanceViewModel _balanceViewModel;
26
26 - final ObservableSet<Erc20Token> tokens;
27 + final ObservableSet<CryptoCurrency> tokens;
28
29 @observable
30 String searchText = '';
@@ -43,7 +44,7 @@ abstract class HomeSettingsViewModelBase with Store {
44 @action
45 void setPinNativeToken(bool value) => _settingsStore.pinNativeTokenAtTop = value;
46
46 - Future<void> addErc20Token(Erc20Token token) async {
47 + Future<void> addToken(CryptoCurrency token) async {
48 if (_balanceViewModel.wallet.type == WalletType.ethereum) {
49 await ethereum!.addErc20Token(_balanceViewModel.wallet, token);
50 }
@@ -52,23 +53,31 @@ abstract class HomeSettingsViewModelBase with Store {
53 await polygon!.addErc20Token(_balanceViewModel.wallet, token);
54 }
55
56 + if (_balanceViewModel.wallet.type == WalletType.solana) {
57 + await solana!.addSPLToken(_balanceViewModel.wallet, token);
58 + }
59 +
60 _updateTokensList();
61 _updateFiatPrices(token);
62 }
63
59 - Future<void> deleteErc20Token(Erc20Token token) async {
64 + Future<void> deleteToken(CryptoCurrency token) async {
65 if (_balanceViewModel.wallet.type == WalletType.ethereum) {
61 - await ethereum!.deleteErc20Token(_balanceViewModel.wallet, token);
66 + await ethereum!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token);
67 }
68
69 if (_balanceViewModel.wallet.type == WalletType.polygon) {
65 - await polygon!.deleteErc20Token(_balanceViewModel.wallet, token);
70 + await polygon!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token);
71 + }
72 +
73 + if (_balanceViewModel.wallet.type == WalletType.solana) {
74 + await solana!.deleteSPLToken(_balanceViewModel.wallet, token);
75 }
76
77 _updateTokensList();
78 }
79
71 - Future<Erc20Token?> getErc20Token(String contractAddress) async {
80 + Future<CryptoCurrency?> getToken(String contractAddress) async {
81 if (_balanceViewModel.wallet.type == WalletType.ethereum) {
82 return await ethereum!.getErc20Token(_balanceViewModel.wallet, contractAddress);
83 }
@@ -77,12 +86,16 @@ abstract class HomeSettingsViewModelBase with Store {
86 return await polygon!.getErc20Token(_balanceViewModel.wallet, contractAddress);
87 }
88
89 + if (_balanceViewModel.wallet.type == WalletType.solana) {
90 + return await solana!.getSPLToken(_balanceViewModel.wallet, contractAddress);
91 + }
92 +
93 return null;
94 }
95
96 CryptoCurrency get nativeToken => _balanceViewModel.wallet.currency;
97
85 - void _updateFiatPrices(Erc20Token token) async {
98 + void _updateFiatPrices(CryptoCurrency token) async {
99 try {
100 _balanceViewModel.fiatConvertationStore.prices[token] =
101 await FiatConversionService.fetchPrice(
@@ -92,20 +105,27 @@ abstract class HomeSettingsViewModelBase with Store {
105 } catch (_) {}
106 }
107
95 - void changeTokenAvailability(Erc20Token token, bool value) async {
108 + void changeTokenAvailability(CryptoCurrency token, bool value) async {
109 token.enabled = value;
110 +
111 if (_balanceViewModel.wallet.type == WalletType.ethereum) {
98 - ethereum!.addErc20Token(_balanceViewModel.wallet, token);
112 + ethereum!.addErc20Token(_balanceViewModel.wallet, token as Erc20Token);
113 }
114 +
115 if (_balanceViewModel.wallet.type == WalletType.polygon) {
101 - polygon!.addErc20Token(_balanceViewModel.wallet, token);
116 + polygon!.addErc20Token(_balanceViewModel.wallet, token as Erc20Token);
117 }
118 +
119 + if (_balanceViewModel.wallet.type == WalletType.solana) {
120 + solana!.addSPLToken(_balanceViewModel.wallet, token);
121 + }
122 +
123 _refreshTokensList();
124 }
125
126 @action
127 void _updateTokensList() {
108 - int _sortFunc(Erc20Token e1, Erc20Token e2) {
128 + int _sortFunc(CryptoCurrency e1, CryptoCurrency e2) {
129 int index1 = _balanceViewModel.formattedBalances.indexWhere((element) => element.asset == e1);
130 int index2 = _balanceViewModel.formattedBalances.indexWhere((element) => element.asset == e2);
131
@@ -138,6 +158,14 @@ abstract class HomeSettingsViewModelBase with Store {
158 .toList()
159 ..sort(_sortFunc));
160 }
161 +
162 + if (_balanceViewModel.wallet.type == WalletType.solana) {
163 + tokens.addAll(solana!
164 + .getSPLTokenCurrencies(_balanceViewModel.wallet)
165 + .where((element) => _matchesSearchText(element))
166 + .toList()
167 + ..sort(_sortFunc));
168 + }
169 }
170
171 @action
@@ -153,10 +181,32 @@ abstract class HomeSettingsViewModelBase with Store {
181 _updateTokensList();
182 }
183
156 - bool _matchesSearchText(Erc20Token asset) {
184 + bool _matchesSearchText(CryptoCurrency asset) {
185 + final address = getTokenAddressBasedOnWallet(asset);
186 +
187 + // The homes settings would only be displayed for either of Ethereum, Polygon or Solana Wallets.
188 + if (address == null) return false;
189 +
190 return searchText.isEmpty ||
191 asset.fullName!.toLowerCase().contains(searchText.toLowerCase()) ||
192 asset.title.toLowerCase().contains(searchText.toLowerCase()) ||
160 - asset.contractAddress == searchText;
193 + address == searchText;
194 + }
195 +
196 + String? getTokenAddressBasedOnWallet(CryptoCurrency asset) {
197 + if (_balanceViewModel.wallet.type == WalletType.solana) {
198 + return solana!.getTokenAddress(asset);
199 + }
200 +
201 + if (_balanceViewModel.wallet.type == WalletType.ethereum) {
202 + return ethereum!.getTokenAddress(asset);
203 + }
204 +
205 + if (_balanceViewModel.wallet.type == WalletType.polygon) {
206 + return polygon!.getTokenAddress(asset);
207 + }
208 +
209 + // 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).
210 + return null;
211 }
212 }
lib/view_model/dashboard/transaction_list_item.dart
+9
@@ -4,6 +4,7 @@ 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/solana/solana.dart';
8 import 'package:cw_core/transaction_direction.dart';
9 import 'package:cw_core/transaction_info.dart';
10 import 'package:cake_wallet/store/settings_store.dart';
@@ -105,6 +106,14 @@ class TransactionListItem extends ActionListItem with Keyable {
106 nano!.getTransactionAmountRaw(transaction).toString(), nanoUtil!.rawPerNano)),
107 price: price);
108 break;
109 + case WalletType.solana:
110 + final asset = solana!.assetOfTransaction(balanceViewModel.wallet, transaction);
111 + final price = balanceViewModel.fiatConvertationStore.prices[asset];
112 + amount = calculateFiatAmountRaw(
113 + cryptoAmount: solana!.getTransactionAmountRaw(transaction),
114 + price: price,
115 + );
116 + break;
117 default:
118 break;
119 }
lib/view_model/exchange/exchange_view_model.dart
+4
@@ -625,6 +625,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
625 depositCurrency = CryptoCurrency.maticpoly;
626 receiveCurrency = CryptoCurrency.xmr;
627 break;
628 + case WalletType.solana:
629 + depositCurrency = CryptoCurrency.sol;
630 + receiveCurrency = CryptoCurrency.xmr;
631 + break;
632 default:
633 break;
634 }
lib/view_model/node_list/node_list_view_model.dart
+3
@@ -75,6 +75,9 @@ abstract class NodeListViewModelBase with Store {
75 case WalletType.polygon:
76 node = getPolygonDefaultNode(nodes: _nodeSource)!;
77 break;
78 + case WalletType.solana:
79 + node = getSolanaDefaultNode(nodes: _nodeSource)!;
80 + break;
81 default:
82 throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}');
83 }
lib/view_model/restore/restore_from_qr_vm.dart
+7
@@ -3,6 +3,7 @@ import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
3 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/view_model/restore/restore_mode.dart';
8 import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
9 import 'package:hive/hive.dart';
@@ -75,6 +76,9 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store
76 case WalletType.polygon:
77 return polygon!.createPolygonRestoreWalletFromPrivateKey(
78 name: name, password: password, privateKey: restoreWallet.privateKey!);
79 + case WalletType.solana:
80 + return solana!.createSolanaRestoreWalletFromPrivateKey(
81 + name: name, password: password, privateKey: restoreWallet.privateKey!);
82 default:
83 throw Exception('Unexpected type: ${restoreWallet.type.toString()}');
84 }
@@ -102,6 +106,9 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store
106 case WalletType.polygon:
107 return polygon!.createPolygonRestoreWalletFromSeedCredentials(
108 name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password);
109 + case WalletType.solana:
110 + return solana!.createSolanaRestoreWalletFromSeedCredentials(
111 + name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password);
112 default:
113 throw Exception('Unexpected type: ${type.toString()}');
114 }
lib/view_model/restore/wallet_restore_from_qr_code.dart
+9
@@ -32,6 +32,7 @@ class WalletRestoreFromQRCode {
32 'bitcoincash': WalletType.bitcoinCash,
33 'bitcoincash-wallet': WalletType.bitcoinCash,
34 'bitcoincash_wallet': WalletType.bitcoinCash,
35 + 'solana-wallet': WalletType.solana,
36 };
37
38 static bool _containsAssetSpecifier(String code) => _extractWalletType(code) != null;
@@ -175,6 +176,14 @@ class WalletRestoreFromQRCode {
176 return WalletRestoreMode.seed;
177 }
178
179 + if (type == WalletType.solana && credentials.containsKey('private_key')) {
180 + final privateKey = credentials['private_key'] as String;
181 + if (privateKey.isEmpty) {
182 + throw Exception('Unexpected restore mode: private_key');
183 + }
184 + return WalletRestoreMode.keys;
185 + }
186 +
187 throw Exception('Unexpected restore mode: restore params are invalid');
188 }
189 }
lib/view_model/send/output.dart
+5 -4
@@ -148,9 +148,8 @@ abstract class OutputBase with Store {
148 @computed
149 String get estimatedFeeFiatAmount {
150 try {
151 - final currency = isEVMCompatibleChain(_wallet.type)
152 - ? _wallet.currency
153 - : cryptoCurrencyHandler();
151 + final currency =
152 + isEVMCompatibleChain(_wallet.type) ? _wallet.currency : cryptoCurrencyHandler();
153 final fiat = calculateFiatAmountRaw(
154 price: _fiatConversationStore.prices[currency]!, cryptoAmount: estimatedFee);
155 return fiat;
@@ -220,7 +219,6 @@ abstract class OutputBase with Store {
219 final crypto = double.parse(fiatAmount.replaceAll(',', '.')) /
220 _fiatConversationStore.prices[cryptoCurrencyHandler()]!;
221 final cryptoAmountTmp = _cryptoNumberFormat.format(crypto);
223 -
222 if (cryptoAmount != cryptoAmountTmp) {
223 cryptoAmount = cryptoAmountTmp;
224 }
@@ -252,6 +250,9 @@ abstract class OutputBase with Store {
250 case WalletType.polygon:
251 maximumFractionDigits = 12;
252 break;
253 + case WalletType.solana:
254 + maximumFractionDigits = 12;
255 + break;
256 default:
257 break;
258 }
lib/view_model/send/send_template_view_model.dart
+2 -1
@@ -52,7 +52,8 @@ abstract class SendTemplateViewModelBase with Store {
52 bool get hasMultiRecipient =>
53 _wallet.type != WalletType.haven &&
54 _wallet.type != WalletType.ethereum &&
55 - _wallet.type != WalletType.polygon;
55 + _wallet.type != WalletType.polygon &&
56 + _wallet.type != WalletType.solana;
57
58 @computed
59 CryptoCurrency get cryptoCurrency => _wallet.currency;
lib/view_model/send/send_view_model.dart
+11 -3
@@ -7,6 +7,7 @@ import 'package:cake_wallet/entities/contact_record.dart';
7 import 'package:cake_wallet/entities/wallet_contact.dart';
8 import 'package:cake_wallet/polygon/polygon.dart';
9 import 'package:cake_wallet/reactions/wallet_connect.dart';
10 +import 'package:cake_wallet/solana/solana.dart';
11 import 'package:cake_wallet/store/app_store.dart';
12 import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
13 import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
@@ -44,7 +45,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
45 void onWalletChange(wallet) {
46 currencies = wallet.balance.keys.toList();
47 selectedCryptoCurrency = wallet.currency;
47 - hasMultipleTokens = isEVMCompatibleChain(wallet.type);
48 + hasMultipleTokens = isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana;
49 }
50
51 SendViewModelBase(
@@ -57,7 +58,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
58 ) : state = InitialExecutionState(),
59 currencies = appStore.wallet!.balance.keys.toList(),
60 selectedCryptoCurrency = appStore.wallet!.currency,
60 - hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type),
61 + hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type) ||
62 + appStore.wallet!.type == WalletType.solana,
63 outputs = ObservableList<Output>(),
64 _settingsStore = appStore.settingsStore,
65 fiatFromSettings = appStore.settingsStore.fiatCurrency,
@@ -100,6 +102,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
102 @computed
103 bool get isBatchSending => outputs.length > 1;
104
105 + bool get shouldDisplaySendALL => walletType != WalletType.solana;
106 +
107 @computed
108 String get pendingTransactionFiatAmount {
109 if (pendingTransaction == null) {
@@ -297,6 +301,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
301 pendingTransaction = await wallet.createTransaction(_credentials());
302 state = ExecutedSuccessfullyState();
303 } catch (e) {
304 + print('Failed with ${e.toString()}');
305 state = FailureState(e.toString());
306 }
307 }
@@ -351,7 +356,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
356 Object _credentials() {
357 final priority = _settingsStore.priority[wallet.type];
358
354 - if (priority == null && wallet.type != WalletType.nano) {
359 + if (priority == null && wallet.type != WalletType.nano && wallet.type != WalletType.solana) {
360 throw Exception('Priority is null for wallet type: ${wallet.type}');
361 }
362
@@ -377,6 +382,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
382 case WalletType.polygon:
383 return polygon!.createPolygonTransactionCredentials(outputs,
384 priority: priority!, currency: selectedCryptoCurrency);
385 + case WalletType.solana:
386 + return solana!
387 + .createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency);
388 default:
389 throw Exception('Unexpected wallet type: ${wallet.type}');
390 }
lib/view_model/settings/other_settings_view_model.dart
+13 -16
@@ -14,15 +14,14 @@ import 'package:package_info/package_info.dart';
14
15 part 'other_settings_view_model.g.dart';
16
17 -class OtherSettingsViewModel = OtherSettingsViewModelBase
18 - with _$OtherSettingsViewModel;
17 +class OtherSettingsViewModel = OtherSettingsViewModelBase with _$OtherSettingsViewModel;
18
19 abstract class OtherSettingsViewModelBase with Store {
20 OtherSettingsViewModelBase(this._settingsStore, this._wallet)
21 : walletType = _wallet.type,
22 currentVersion = '' {
24 - PackageInfo.fromPlatform().then(
25 - (PackageInfo packageInfo) => currentVersion = packageInfo.version);
23 + PackageInfo.fromPlatform()
24 + .then((PackageInfo packageInfo) => currentVersion = packageInfo.version);
25
26 final priority = _settingsStore.priority[_wallet.type];
27 final priorities = priorityForWalletType(_wallet.type);
@@ -33,8 +32,7 @@ abstract class OtherSettingsViewModelBase with Store {
32 }
33
34 final WalletType walletType;
36 - final WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
37 - TransactionInfo> _wallet;
35 + final WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> _wallet;
36
37 @observable
38 String currentVersion;
@@ -57,12 +55,14 @@ abstract class OtherSettingsViewModelBase with Store {
55 _wallet.type == WalletType.nano || _wallet.type == WalletType.banano;
56
57 @computed
60 - bool get isEnabledBuyAction =>
61 - !_settingsStore.disableBuy && _wallet.type != WalletType.haven;
58 + bool get displayTransactionPriority =>
59 + !(changeRepresentativeEnabled || _wallet.type == WalletType.solana);
60
61 @computed
64 - bool get isEnabledSellAction =>
65 - !_settingsStore.disableSell && _wallet.type != WalletType.haven;
62 + bool get isEnabledBuyAction => !_settingsStore.disableBuy && _wallet.type != WalletType.haven;
63 +
64 + @computed
65 + bool get isEnabledSellAction => !_settingsStore.disableSell && _wallet.type != WalletType.haven;
66
67 List<ProviderType> get availableBuyProvidersTypes {
68 return ProvidersHelper.getAvailableBuyProviderTypes(walletType);
@@ -72,12 +72,10 @@ abstract class OtherSettingsViewModelBase with Store {
72 ProvidersHelper.getAvailableSellProviderTypes(walletType);
73
74 ProviderType get buyProviderType =>
75 - _settingsStore.defaultBuyProviders[walletType] ??
76 - ProviderType.askEachTime;
75 + _settingsStore.defaultBuyProviders[walletType] ?? ProviderType.askEachTime;
76
77 ProviderType get sellProviderType =>
79 - _settingsStore.defaultSellProviders[walletType] ??
80 - ProviderType.askEachTime;
78 + _settingsStore.defaultSellProviders[walletType] ?? ProviderType.askEachTime;
79
80 String getDisplayPriority(dynamic priority) {
81 final _priority = priority as TransactionPriority;
@@ -114,7 +112,6 @@ abstract class OtherSettingsViewModelBase with Store {
112 _settingsStore.defaultBuyProviders[walletType] = buyProviderType;
113
114 @action
117 - ProviderType onSellProviderTypeSelected(
118 - ProviderType sellProviderType) =>
115 + ProviderType onSellProviderTypeSelected(ProviderType sellProviderType) =>
116 _settingsStore.defaultSellProviders[walletType] = sellProviderType;
117 }
lib/view_model/transaction_details_view_model.dart
+24
@@ -54,6 +54,9 @@ abstract class TransactionDetailsViewModelBase with Store {
54 case WalletType.polygon:
55 _addPolygonListItems(tx, dateFormat);
56 break;
57 + case WalletType.solana:
58 + _addSolanaListItems(tx, dateFormat);
59 + break;
60 default:
61 break;
62 }
@@ -131,6 +134,8 @@ abstract class TransactionDetailsViewModelBase with Store {
134 return 'https://bananolooker.com/block/${txId}';
135 case WalletType.polygon:
136 return 'https://polygonscan.com/tx/${txId}';
137 + case WalletType.solana:
138 + return 'https://solscan.io/tx/${txId}';
139 default:
140 return '';
141 }
@@ -155,6 +160,8 @@ abstract class TransactionDetailsViewModelBase with Store {
160 return S.current.view_transaction_on + 'bananolooker.com';
161 case WalletType.polygon:
162 return S.current.view_transaction_on + 'polygonscan.com';
163 + case WalletType.solana:
164 + return S.current.view_transaction_on + 'solscan.io';
165 default:
166 return '';
167 }
@@ -281,4 +288,21 @@ abstract class TransactionDetailsViewModelBase with Store {
288
289 items.addAll(_items);
290 }
291 +
292 + void _addSolanaListItems(TransactionInfo tx, DateFormat dateFormat) {
293 + final _items = [
294 + StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id),
295 + StandartListItem(
296 + title: S.current.transaction_details_date, value: dateFormat.format(tx.date)),
297 + StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()),
298 + if (tx.feeFormatted()?.isNotEmpty ?? false)
299 + StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!),
300 + if (showRecipientAddress && tx.to != null)
301 + StandartListItem(title: S.current.transaction_details_recipient_address, value: tx.to!),
302 + if (tx.from != null)
303 + StandartListItem(title: S.current.transaction_details_source_address, value: tx.from!),
304 + ];
305 +
306 + items.addAll(_items);
307 + }
308 }
lib/view_model/wallet_address_list/wallet_address_list_view_model.dart
+26
@@ -7,6 +7,7 @@ import 'package:cake_wallet/generated/i18n.dart';
7 import 'package:cake_wallet/haven/haven.dart';
8 import 'package:cake_wallet/monero/monero.dart';
9 import 'package:cake_wallet/polygon/polygon.dart';
10 +import 'package:cake_wallet/solana/solana.dart';
11 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';
@@ -159,6 +160,21 @@ class PolygonURI extends PaymentURI {
160 }
161 }
162
163 +class SolanaURI extends PaymentURI {
164 + SolanaURI({required String amount, required String address})
165 + : super(amount: amount, address: address);
166 +
167 + @override
168 + String toString() {
169 + var base = 'solana:' + address;
170 + if (amount.isNotEmpty) {
171 + base += '?amount=${amount.replaceAll(',', '.')}';
172 + }
173 +
174 + return base;
175 + }
176 +}
177 +
178 abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store {
179 WalletAddressListViewModelBase({
180 required AppStore appStore,
@@ -257,6 +273,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
273 return PolygonURI(amount: amount, address: address.address);
274 }
275
276 + if (wallet.type == WalletType.solana) {
277 + return SolanaURI(amount: amount, address: address.address);
278 + }
279 +
280 throw Exception('Unexpected type: ${type.toString()}');
281 }
282
@@ -326,6 +346,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
346 addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress));
347 }
348
349 + if (wallet.type == WalletType.solana) {
350 + final primaryAddress = solana!.getAddress(wallet);
351 +
352 + addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress));
353 + }
354 +
355 if (searchText.isNotEmpty) {
356 return ObservableList.of(addressList.where((item) {
357 if (item is WalletAddressListItem) {
lib/view_model/wallet_keys_view_model.dart
+4 -1
@@ -110,7 +110,8 @@ abstract class WalletKeysViewModelBase with Store {
110 ]);
111 }
112
113 - if (isEVMCompatibleChain(_appStore.wallet!.type)) {
113 + if (isEVMCompatibleChain(_appStore.wallet!.type) ||
114 + _appStore.wallet!.type == WalletType.solana) {
115 items.addAll([
116 if (_appStore.wallet!.privateKey != null)
117 StandartListItem(title: S.current.private_key, value: _appStore.wallet!.privateKey!),
@@ -165,6 +166,8 @@ abstract class WalletKeysViewModelBase with Store {
166 return 'banano-wallet';
167 case WalletType.polygon:
168 return 'polygon-wallet';
169 + case WalletType.solana:
170 + return 'solana-wallet';
171 default:
172 throw Exception('Unexpected wallet type: ${_appStore.wallet!.toString()}');
173 }
lib/view_model/wallet_new_vm.dart
+19 -14
@@ -1,5 +1,6 @@
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:hive/hive.dart';
5 import 'package:mobx/mobx.dart';
6 import 'package:cake_wallet/monero/monero.dart';
@@ -36,19 +37,21 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
37 bool get hasLanguageSelector => type == WalletType.monero || type == WalletType.haven;
38
39 int get seedPhraseWordsLength {
39 - switch (type) {
40 - case WalletType.monero:
41 - if(advancedPrivacySettingsViewModel.isPolySeed) {
42 - return 16;
43 - }
44 - return 25;
45 - case WalletType.ethereum:
46 - case WalletType.bitcoinCash:
47 - return advancedPrivacySettingsViewModel.seedPhraseLength.value;
48 - default:
49 - return 24;
50 - }
40 + switch (type) {
41 + case WalletType.monero:
42 + if (advancedPrivacySettingsViewModel.isPolySeed) {
43 + return 16;
44 + }
45 + return 25;
46 + case WalletType.solana:
47 + case WalletType.polygon:
48 + case WalletType.ethereum:
49 + case WalletType.bitcoinCash:
50 + return advancedPrivacySettingsViewModel.seedPhraseLength.value;
51 + default:
52 + return 24;
53 }
54 + }
55
56 bool get hasSeedType => type == WalletType.monero;
57
@@ -64,8 +67,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
67 case WalletType.litecoin:
68 return bitcoin!.createBitcoinNewWalletCredentials(name: name);
69 case WalletType.haven:
67 - return haven!.createHavenNewWalletCredentials(
68 - name: name, language: options!.first as String);
70 + return haven!
71 + .createHavenNewWalletCredentials(name: name, language: options!.first as String);
72 case WalletType.ethereum:
73 return ethereum!.createEthereumNewWalletCredentials(name: name);
74 case WalletType.bitcoinCash:
@@ -74,6 +77,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
77 return nano!.createNanoNewWalletCredentials(name: name);
78 case WalletType.polygon:
79 return polygon!.createPolygonNewWalletCredentials(name: name);
80 + case WalletType.solana:
81 + return solana!.createSolanaNewWalletCredentials(name: name);
82 default:
83 throw Exception('Unexpected type: ${type.toString()}');
84 }
lib/view_model/wallet_restore_view_model.dart
+25 -20
@@ -4,6 +4,7 @@ import 'package:cake_wallet/nano/nano.dart';
4 import 'package:cake_wallet/ethereum/ethereum.dart';
5 import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
6 import 'package:cake_wallet/polygon/polygon.dart';
7 +import 'package:cake_wallet/solana/solana.dart';
8 import 'package:hive/hive.dart';
9 import 'package:mobx/mobx.dart';
10 import 'package:cake_wallet/store/app_store.dart';
@@ -28,11 +29,11 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
29 {required WalletType type})
30 : hasSeedLanguageSelector = type == WalletType.monero || type == WalletType.haven,
31 hasBlockchainHeightLanguageSelector = type == WalletType.monero || type == WalletType.haven,
31 - hasRestoreFromPrivateKey =
32 - type == WalletType.ethereum ||
32 + hasRestoreFromPrivateKey = type == WalletType.ethereum ||
33 type == WalletType.polygon ||
34 type == WalletType.nano ||
35 - type == WalletType.banano,
35 + type == WalletType.banano ||
36 + type == WalletType.solana,
37 isButtonEnabled = false,
38 mode = WalletRestoreMode.seed,
39 super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true) {
@@ -45,6 +46,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
46 break;
47 case WalletType.nano:
48 case WalletType.banano:
49 + case WalletType.solana:
50 availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys];
51 break;
52 default:
@@ -98,22 +100,21 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
100 name: name, height: height, mnemonic: seed, password: password);
101 case WalletType.ethereum:
102 return ethereum!.createEthereumRestoreWalletFromSeedCredentials(
101 - name: name,
102 - mnemonic: seed,
103 - password: password);
103 + name: name, mnemonic: seed, password: password);
104 case WalletType.bitcoinCash:
105 return bitcoinCash!.createBitcoinCashRestoreWalletFromSeedCredentials(
106 - name: name,
107 - mnemonic: seed,
108 - password: password);
106 + name: name, mnemonic: seed, password: password);
107 case WalletType.nano:
108 return nano!.createNanoRestoreWalletFromSeedCredentials(
109 + name: name, mnemonic: seed, password: password, derivationType: derivationType);
110 + case WalletType.polygon:
111 + return polygon!.createPolygonRestoreWalletFromSeedCredentials(
112 name: name,
113 mnemonic: seed,
114 password: password,
114 - derivationType: derivationType);
115 - case WalletType.polygon:
116 - return polygon!.createPolygonRestoreWalletFromSeedCredentials(
115 + );
116 + case WalletType.solana:
117 + return solana!.createSolanaRestoreWalletFromSeedCredentials(
118 name: name,
119 mnemonic: seed,
120 password: password,
@@ -160,16 +161,22 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
161
162 case WalletType.nano:
163 return nano!.createNanoRestoreWalletFromKeysCredentials(
163 - name: name,
164 - password: password,
165 - seedKey: options['private_key'] as String,
166 - derivationType: options["derivationType"] as DerivationType);
164 + name: name,
165 + password: password,
166 + seedKey: options['private_key'] as String,
167 + derivationType: options["derivationType"] as DerivationType);
168 case WalletType.polygon:
169 return polygon!.createPolygonRestoreWalletFromPrivateKey(
170 name: name,
171 password: password,
172 privateKey: options['private_key'] as String,
173 );
174 + case WalletType.solana:
175 + return solana!.createSolanaRestoreWalletFromPrivateKey(
176 + name: name,
177 + password: password,
178 + privateKey: options['private_key'] as String,
179 + );
180 default:
181 break;
182 }
@@ -187,10 +194,8 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
194
195 switch (type) {
196 case WalletType.nano:
190 - return nanoUtil!.compareDerivationMethods(
191 - mnemonic: mnemonic,
192 - privateKey: seedKey,
193 - node: node);
197 + return nanoUtil!
198 + .compareDerivationMethods(mnemonic: mnemonic, privateKey: seedKey, node: node);
199 default:
200 break;
201 }
model_generator.sh
+1
@@ -5,6 +5,7 @@ cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build
5 cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
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_ethereum && flutter pub get && cd ..
10 cd cw_polygon && flutter pub get && cd ..
11 flutter packages pub run build_runner build --delete-conflicting-outputs
pubspec_base.yaml
+2
@@ -106,6 +106,7 @@ dependencies:
106 flutter_svg: ^2.0.9
107 polyseed: ^0.0.2
108 nostr_tools: ^1.0.9
109 + solana: ^0.30.1
110
111 dev_dependencies:
112 flutter_test:
@@ -152,6 +153,7 @@ flutter:
153 - assets/nano_node_list.yml
154 - assets/nano_pow_node_list.yml
155 - assets/polygon_node_list.yml
156 + - assets/solana_node_list.yml
157 - assets/text/
158 - assets/faq/
159 - assets/animation/
res/values/strings_ar.arb
+3 -2
@@ -778,5 +778,6 @@
778 "you_pay": "انت تدفع",
779 "you_will_get": "حول الى",
780 "you_will_send": "تحويل من",
781 - "yy": "YY"
782 -}
\ No newline at end of file
781 + "yy": "YY",
782 + "waitFewSecondForTxUpdate": "ﺕﻼﻣﺎﻌﻤﻟﺍ ﻞﺠﺳ ﻲﻓ ﺔﻠﻣﺎﻌﻤﻟﺍ ﺲﻜﻌﻨﺗ ﻰﺘﺣ ﻥﺍﻮﺛ ﻊﻀﺒﻟ ﺭﺎﻈﺘﻧﻻﺍ ﻰﺟﺮﻳ"
783 +}
res/values/strings_bg.arb
+3 -2
@@ -778,5 +778,6 @@
778 "you_pay": "Вие плащате",
779 "you_will_get": "Обръщане в",
780 "you_will_send": "Обръщане от",
781 - "yy": "гг"
782 -}
\ No newline at end of file
781 + "yy": "гг",
782 + "waitFewSecondForTxUpdate": "Моля, изчакайте няколко секунди, докато транзакцията се отрази в историята на транзакциите"
783 +}
res/values/strings_cs.arb
+3 -2
@@ -778,5 +778,6 @@
778 "you_pay": "Zaplatíte",
779 "you_will_get": "Směnit na",
780 "you_will_send": "Směnit z",
781 - "yy": "YY"
782 -}
\ No newline at end of file
781 + "yy": "YY",
782 + "waitFewSecondForTxUpdate": "Počkejte několik sekund, než se transakce projeví v historii transakcí"
783 +}
res/values/strings_de.arb
+3 -2
@@ -781,5 +781,6 @@
781 "you_pay": "Sie bezahlen",
782 "you_will_get": "Konvertieren zu",
783 "you_will_send": "Konvertieren von",
784 - "yy": "YY"
785 -}
\ No newline at end of file
784 + "yy": "YY",
785 + "waitFewSecondForTxUpdate": "Bitte warten Sie einige Sekunden, bis die Transaktion im Transaktionsverlauf angezeigt wird"
786 +}
res/values/strings_en.arb
+3 -2
@@ -778,5 +778,6 @@
778 "you_pay": "You Pay",
779 "you_will_get": "Convert to",
780 "you_will_send": "Convert from",
781 - "yy": "YY"
782 -}
\ No newline at end of file
781 + "yy": "YY",
782 + "waitFewSecondForTxUpdate": "Kindly wait for a few seconds for transaction to reflect in transactions history"
783 +}
res/values/strings_es.arb
+3 -2
@@ -779,5 +779,6 @@
779 "you_pay": "Tú pagas",
780 "you_will_get": "Convertir a",
781 "you_will_send": "Convertir de",
782 - "yy": "YY"
783 -}
\ No newline at end of file
782 + "yy": "YY",
783 + "waitFewSecondForTxUpdate": "Espere unos segundos para que la transacción se refleje en el historial de transacciones."
784 +}
res/values/strings_fr.arb
+3 -2
@@ -778,5 +778,6 @@
778 "you_pay": "Vous payez",
779 "you_will_get": "Convertir vers",
780 "you_will_send": "Convertir depuis",
781 - "yy": "AA"
782 -}
\ No newline at end of file
781 + "yy": "AA",
782 + "waitFewSecondForTxUpdate": "Veuillez attendre quelques secondes pour que la transaction soit reflétée dans l'historique des transactions."
783 +}
res/values/strings_ha.arb
+3 -2
@@ -780,5 +780,6 @@
780 "you_pay": "Ka Bayar",
781 "you_will_get": "Maida zuwa",
782 "you_will_send": "Maida daga",
783 - "yy": "YY"
784 -}
\ No newline at end of file
783 + "yy": "YY",
784 + "waitFewSecondForTxUpdate": "Da fatan za a jira ƴan daƙiƙa don ciniki don yin tunani a tarihin ma'amala"
785 +}
res/values/strings_hi.arb
+3 -2
@@ -780,5 +780,6 @@
780 "you_pay": "आप भुगतान करते हैं",
781 "you_will_get": "में बदलें",
782 "you_will_send": "से रूपांतरित करें",
783 - "yy": "वाईवाई"
784 -}
\ No newline at end of file
783 + "yy": "वाईवाई",
784 + "waitFewSecondForTxUpdate": "लेन-देन इतिहास में लेन-देन प्रतिबिंबित होने के लिए कृपया कुछ सेकंड प्रतीक्षा करें"
785 +}
res/values/strings_hr.arb
+3 -2
@@ -778,5 +778,6 @@
778 "you_pay": "Vi plaćate",
779 "you_will_get": "Razmijeni u",
780 "you_will_send": "Razmijeni iz",
781 - "yy": "GG"
782 -}
\ No newline at end of file
781 + "yy": "GG",
782 + "waitFewSecondForTxUpdate": "Pričekajte nekoliko sekundi da se transakcija prikaže u povijesti transakcija"
783 +}
res/values/strings_id.arb
+3 -2
@@ -781,5 +781,6 @@
781 "you_pay": "Anda Membayar",
782 "you_will_get": "Konversi ke",
783 "you_will_send": "Konversi dari",
784 - "yy": "YY"
785 -}
\ No newline at end of file
784 + "yy": "YY",
785 + "waitFewSecondForTxUpdate": "Mohon tunggu beberapa detik hingga transaksi terlihat di riwayat transaksi"
786 +}
res/values/strings_it.arb
+3 -2
@@ -781,5 +781,6 @@
781 "you_pay": "Tu paghi",
782 "you_will_get": "Converti a",
783 "you_will_send": "Conveti da",
784 - "yy": "YY"
785 -}
\ No newline at end of file
784 + "yy": "YY",
785 + "waitFewSecondForTxUpdate": "Attendi qualche secondo affinché la transazione venga riflessa nella cronologia delle transazioni"
786 +}
res/values/strings_ja.arb
+3 -2
@@ -779,5 +779,6 @@
779 "you_pay": "あなたが支払う",
780 "you_will_get": "に変換",
781 "you_will_send": "から変換",
782 - "yy": "YY"
783 -}
\ No newline at end of file
782 + "yy": "YY",
783 + "waitFewSecondForTxUpdate": "取引履歴に取引が反映されるまで数秒お待ちください。"
784 +}
res/values/strings_ko.arb
+3 -2
@@ -780,5 +780,6 @@
780 "you_will_get": "로 변환하다",
781 "you_will_send": "다음에서 변환",
782 "YY": "YY",
783 - "yy": "YY"
784 -}
\ No newline at end of file
783 + "yy": "YY",
784 + "waitFewSecondForTxUpdate": "거래 내역에 거래가 반영될 때까지 몇 초 정도 기다려 주세요."
785 +}
res/values/strings_my.arb
+3 -2
@@ -778,5 +778,6 @@
778 "you_pay": "သင်ပေးချေပါ။",
779 "you_will_get": "သို့ပြောင်းပါ။",
780 "you_will_send": "မှပြောင်းပါ။",
781 - "yy": "YY"
782 -}
\ No newline at end of file
781 + "yy": "YY",
782 + "waitFewSecondForTxUpdate": "ငွေပေးငွေယူ မှတ်တမ်းတွင် ရောင်ပြန်ဟပ်ရန် စက္ကန့်အနည်းငယ်စောင့်ပါ။"
783 +}
res/values/strings_nl.arb
+3 -2
@@ -779,5 +779,6 @@
779 "you_pay": "U betaalt",
780 "you_will_get": "Converteren naar",
781 "you_will_send": "Converteren van",
782 - "yy": "JJ"
783 -}
\ No newline at end of file
782 + "yy": "JJ",
783 + "waitFewSecondForTxUpdate": "Wacht een paar seconden totdat de transactie wordt weergegeven in de transactiegeschiedenis"
784 +}
res/values/strings_pl.arb
+3 -2
@@ -778,5 +778,6 @@
778 "you_pay": "Płacisz",
779 "you_will_get": "Konwertuj na",
780 "you_will_send": "Konwertuj z",
781 - "yy": "RR"
782 -}
\ No newline at end of file
781 + "yy": "RR",
782 + "waitFewSecondForTxUpdate": "Poczekaj kilka sekund, aż transakcja zostanie odzwierciedlona w historii transakcji"
783 +}
res/values/strings_pt.arb
+3 -2
@@ -781,5 +781,6 @@
781 "you_pay": "Você paga",
782 "you_will_get": "Converter para",
783 "you_will_send": "Converter de",
784 - "yy": "aa"
785 -}
\ No newline at end of file
784 + "yy": "aa",
785 + "waitFewSecondForTxUpdate": "Aguarde alguns segundos para que a transação seja refletida no histórico de transações"
786 +}
res/values/strings_ru.arb
+3 -2
@@ -779,5 +779,6 @@
779 "you_pay": "Вы платите",
780 "you_will_get": "Конвертировать в",
781 "you_will_send": "Конвертировать из",
782 - "yy": "ГГ"
783 -}
\ No newline at end of file
782 + "yy": "ГГ",
783 + "waitFewSecondForTxUpdate": "Пожалуйста, подождите несколько секунд, чтобы транзакция отразилась в истории транзакций."
784 +}
res/values/strings_th.arb
+3 -2
@@ -778,5 +778,6 @@
778 "you_pay": "คุณจ่าย",
779 "you_will_get": "แปลงเป็น",
780 "you_will_send": "แปลงจาก",
781 - "yy": "ปี"
782 -}
\ No newline at end of file
781 + "yy": "ปี",
782 + "waitFewSecondForTxUpdate": "กรุณารอสักครู่เพื่อให้ธุรกรรมปรากฏในประวัติการทำธุรกรรม"
783 +}
res/values/strings_tl.arb
+3 -2
@@ -778,5 +778,6 @@
778 "you_pay": "Magbabayad ka",
779 "you_will_get": "Mag -convert sa",
780 "you_will_send": "I -convert mula sa",
781 - "yy": "YY"
782 -}
\ No newline at end of file
781 + "yy": "YY",
782 + "waitFewSecondForTxUpdate": "Mangyaring maghintay ng ilang segundo para makita ang transaksyon sa history ng mga transaksyon"
783 +}
res/values/strings_tr.arb
+3 -2
@@ -778,5 +778,6 @@
778 "you_pay": "Şu kadar ödeyeceksin: ",
779 "you_will_get": "Biçimine dönüştür:",
780 "you_will_send": "Biçiminden dönüştür:",
781 - "yy": "YY"
782 -}
\ No newline at end of file
781 + "yy": "YY",
782 + "waitFewSecondForTxUpdate": "İşlemin işlem geçmişine yansıması için lütfen birkaç saniye bekleyin"
783 +}
res/values/strings_uk.arb
+3 -2
@@ -779,5 +779,6 @@
779 "you_pay": "Ви платите",
780 "you_will_get": "Конвертувати в",
781 "you_will_send": "Конвертувати з",
782 - "yy": "YY"
783 -}
\ No newline at end of file
782 + "yy": "YY",
783 + "waitFewSecondForTxUpdate": "Будь ласка, зачекайте кілька секунд, поки транзакція відобразиться в історії транзакцій"
784 +}
res/values/strings_ur.arb
+3 -2
@@ -780,5 +780,6 @@
780 "you_pay": "تم ادا کرو",
781 "you_will_get": "میں تبدیل کریں۔",
782 "you_will_send": "سے تبدیل کریں۔",
783 - "yy": "YY"
784 -}
\ No newline at end of file
783 + "yy": "YY",
784 + "waitFewSecondForTxUpdate": "۔ﮟﯾﺮﮐ ﺭﺎﻈﺘﻧﺍ ﺎﮐ ﮉﻨﮑﯿﺳ ﺪﻨﭼ ﻡﺮﮐ ﮦﺍﺮﺑ ﮯﯿﻟ ﮯﮐ ﮯﻧﺮﮐ ﯽﺳﺎﮑﻋ ﯽﮐ ﻦﯾﺩ ﻦﯿﻟ ﮟﯿﻣ ﺦﯾﺭﺎﺗ ﯽﮐ ﻦ"
785 +}
res/values/strings_yo.arb
+3 -2
@@ -779,5 +779,6 @@
779 "you_pay": "Ẹ sàn",
780 "you_will_get": "Ṣe pàṣípààrọ̀ sí",
781 "you_will_send": "Ṣe pàṣípààrọ̀ láti",
782 - "yy": "Ọd"
783 -}
\ No newline at end of file
782 + "yy": "Ọd",
783 + "waitFewSecondForTxUpdate": "Fi inurere duro fun awọn iṣeju diẹ fun idunadura lati ṣe afihan ninu itan-akọọlẹ iṣowo"
784 +}
res/values/strings_zh.arb
+3 -2
@@ -778,5 +778,6 @@
778 "you_pay": "你付钱",
779 "you_will_get": "转换到",
780 "you_will_send": "转换自",
781 - "yy": "YY"
782 -}
\ No newline at end of file
781 + "yy": "YY",
782 + "waitFewSecondForTxUpdate": "请等待几秒钟,交易才会反映在交易历史记录中"
783 +}
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"
13 + CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana"
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"
31 + CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana"
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";; #--haven
34 + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana";; #--haven
35 esac
36
37 cp -rf pubspec_description.yaml pubspec.yaml
tool/configure.dart
+107 -6
@@ -7,6 +7,7 @@ const ethereumOutputPath = 'lib/ethereum/ethereum.dart';
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 walletTypesPath = 'lib/wallet_types.g.dart';
12 const pubspecDefaultPath = 'pubspec_default.yaml';
13 const pubspecOutputPath = 'pubspec.yaml';
@@ -21,6 +22,7 @@ Future<void> main(List<String> args) async {
22 final hasNano = args.contains('${prefix}nano');
23 final hasBanano = args.contains('${prefix}banano');
24 final hasPolygon = args.contains('${prefix}polygon');
25 + final hasSolana = args.contains('${prefix}solana');
26
27 await generateBitcoin(hasBitcoin);
28 await generateMonero(hasMonero);
@@ -29,6 +31,7 @@ Future<void> main(List<String> args) async {
31 await generateBitcoinCash(hasBitcoinCash);
32 await generateNano(hasNano);
33 await generatePolygon(hasPolygon);
34 + await generateSolana(hasSolana);
35 // await generateBanano(hasEthereum);
36
37 await generatePubspec(
@@ -40,6 +43,7 @@ Future<void> main(List<String> args) async {
43 hasBanano: hasBanano,
44 hasBitcoinCash: hasBitcoinCash,
45 hasPolygon: hasPolygon,
46 + hasSolana: hasSolana,
47 );
48 await generateWalletTypes(
49 hasMonero: hasMonero,
@@ -50,6 +54,7 @@ Future<void> main(List<String> args) async {
54 hasBanano: hasBanano,
55 hasBitcoinCash: hasBitcoinCash,
56 hasPolygon: hasPolygon,
57 + hasSolana: hasSolana,
58 );
59 }
60
@@ -577,13 +582,14 @@ abstract class Ethereum {
582 int formatterEthereumParseAmount(String amount);
583 double formatterEthereumAmountToDouble({TransactionInfo? transaction, BigInt? amount, int exponent = 18});
584 List<Erc20Token> getERC20Currencies(WalletBase wallet);
580 - Future<void> addErc20Token(WalletBase wallet, Erc20Token token);
581 - Future<void> deleteErc20Token(WalletBase wallet, Erc20Token token);
585 + Future<void> addErc20Token(WalletBase wallet, CryptoCurrency token);
586 + Future<void> deleteErc20Token(WalletBase wallet, CryptoCurrency token);
587 Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress);
588
589 CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction);
590 void updateEtherscanUsageState(WalletBase wallet, bool isEnabled);
591 Web3Client? getWeb3Client(WalletBase wallet);
592 + String getTokenAddress(CryptoCurrency asset);
593 }
594 """;
595
@@ -669,13 +675,14 @@ abstract class Polygon {
675 int formatterPolygonParseAmount(String amount);
676 double formatterPolygonAmountToDouble({TransactionInfo? transaction, BigInt? amount, int exponent = 18});
677 List<Erc20Token> getERC20Currencies(WalletBase wallet);
672 - Future<void> addErc20Token(WalletBase wallet, Erc20Token token);
673 - Future<void> deleteErc20Token(WalletBase wallet, Erc20Token token);
678 + Future<void> addErc20Token(WalletBase wallet, CryptoCurrency token);
679 + Future<void> deleteErc20Token(WalletBase wallet, CryptoCurrency token);
680 Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress);
681
682 CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction);
683 void updatePolygonScanUsageState(WalletBase wallet, bool isEnabled);
684 Web3Client? getWeb3Client(WalletBase wallet);
685 + String getTokenAddress(CryptoCurrency asset);
686 }
687 """;
688
@@ -885,6 +892,86 @@ abstract class NanoUtil {
892 await outputFile.writeAsString(output);
893 }
894
895 +Future<void> generateSolana(bool hasImplementation) async {
896 + final outputFile = File(solanaOutputPath);
897 + const solanaCommonHeaders = """
898 +import 'package:cake_wallet/view_model/send/output.dart';
899 +import 'package:cw_core/crypto_currency.dart';
900 +import 'package:cw_core/output_info.dart';
901 +import 'package:cw_core/transaction_info.dart';
902 +import 'package:cw_core/wallet_base.dart';
903 +import 'package:cw_core/wallet_credentials.dart';
904 +import 'package:cw_core/wallet_info.dart';
905 +import 'package:cw_core/wallet_service.dart';
906 +import 'package:hive/hive.dart';
907 +
908 +""";
909 + const solanaCWHeaders = """
910 +import 'package:cw_solana/spl_token.dart';
911 +import 'package:cw_solana/solana_wallet.dart';
912 +import 'package:cw_solana/solana_mnemonics.dart';
913 +import 'package:cw_solana/solana_wallet_service.dart';
914 +import 'package:cw_solana/solana_transaction_info.dart';
915 +import 'package:cw_solana/solana_transaction_credentials.dart';
916 +import 'package:cw_solana/solana_wallet_creation_credentials.dart';
917 +import 'package:solana/solana.dart';
918 +""";
919 + const solanaCwPart = "part 'cw_solana.dart';";
920 + const solanaContent = """
921 +abstract class Solana {
922 + List<String> getSolanaWordList(String language);
923 + WalletService createSolanaWalletService(Box<WalletInfo> walletInfoSource);
924 + WalletCredentials createSolanaNewWalletCredentials(
925 + {required String name, WalletInfo? walletInfo});
926 + WalletCredentials createSolanaRestoreWalletFromSeedCredentials(
927 + {required String name, required String mnemonic, required String password});
928 + WalletCredentials createSolanaRestoreWalletFromPrivateKey(
929 + {required String name, required String privateKey, required String password});
930 +
931 + String getAddress(WalletBase wallet);
932 + String getPrivateKey(WalletBase wallet);
933 + String getPublicKey(WalletBase wallet);
934 + Ed25519HDKeyPair? getWalletKeyPair(WalletBase wallet);
935 +
936 + Object createSolanaTransactionCredentials(
937 + List<Output> outputs, {
938 + required CryptoCurrency currency,
939 + });
940 +
941 + Object createSolanaTransactionCredentialsRaw(
942 + List<OutputInfo> outputs, {
943 + required CryptoCurrency currency,
944 + });
945 + List<SPLToken> getSPLTokenCurrencies(WalletBase wallet);
946 + Future<void> addSPLToken(WalletBase wallet, CryptoCurrency token);
947 + Future<void> deleteSPLToken(WalletBase wallet, CryptoCurrency token);
948 + Future<SPLToken?> getSPLToken(WalletBase wallet, String contractAddress);
949 +
950 + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction);
951 + double getTransactionAmountRaw(TransactionInfo transactionInfo);
952 + String getTokenAddress(CryptoCurrency asset);
953 + List<int>? getValidationLength(CryptoCurrency type);
954 +}
955 +
956 + """;
957 +
958 + const solanaEmptyDefinition = 'Solana? solana;\n';
959 + const solanaCWDefinition = 'Solana? solana = CWSolana();\n';
960 +
961 + final output = '$solanaCommonHeaders\n' +
962 + (hasImplementation ? '$solanaCWHeaders\n' : '\n') +
963 + (hasImplementation ? '$solanaCwPart\n\n' : '\n') +
964 + (hasImplementation ? solanaCWDefinition : solanaEmptyDefinition) +
965 + '\n' +
966 + solanaContent;
967 +
968 + if (outputFile.existsSync()) {
969 + await outputFile.delete();
970 + }
971 +
972 + await outputFile.writeAsString(output);
973 +}
974 +
975 Future<void> generatePubspec(
976 {required bool hasMonero,
977 required bool hasBitcoin,
@@ -893,7 +980,8 @@ Future<void> generatePubspec(
980 required bool hasNano,
981 required bool hasBanano,
982 required bool hasBitcoinCash,
896 - required bool hasPolygon}) async {
983 + required bool hasPolygon,
984 + required bool hasSolana}) async {
985 const cwCore = """
986 cw_core:
987 path: ./cw_core
@@ -934,6 +1022,10 @@ Future<void> generatePubspec(
1022 cw_polygon:
1023 path: ./cw_polygon
1024 """;
1025 + const cwSolana = """
1026 + cw_solana:
1027 + path: ./cw_solana
1028 + """;
1029 const cwEVM = """
1030 cw_evm:
1031 path: ./cw_evm
@@ -972,6 +1064,10 @@ Future<void> generatePubspec(
1064 output += '\n$cwPolygon';
1065 }
1066
1067 + if (hasSolana) {
1068 + output += '\n$cwSolana';
1069 + }
1070 +
1071 if (hasHaven && !hasMonero) {
1072 output += '\n$cwSharedExternal\n$cwHaven';
1073 } else if (hasHaven) {
@@ -1002,7 +1098,8 @@ Future<void> generateWalletTypes(
1098 required bool hasNano,
1099 required bool hasBanano,
1100 required bool hasBitcoinCash,
1005 - required bool hasPolygon}) async {
1101 + required bool hasPolygon,
1102 + required bool hasSolana}) async {
1103 final walletTypesFile = File(walletTypesPath);
1104
1105 if (walletTypesFile.existsSync()) {
@@ -1037,6 +1134,10 @@ Future<void> generateWalletTypes(
1134 outputContent += '\tWalletType.polygon,\n';
1135 }
1136
1137 + if (hasSolana) {
1138 + outputContent += '\tWalletType.solana,\n';
1139 + }
1140 +
1141 if (hasNano) {
1142 outputContent += '\tWalletType.nano,\n';
1143 }
tool/generate_secrets_config.dart
+19 -1
@@ -5,6 +5,7 @@ import 'utils/utils.dart';
5
6 const configPath = 'tool/.secrets-config.json';
7 const evmChainsConfigPath = 'tool/.evm-secrets-config.json';
8 +const solanaConfigPath = 'tool/.solana-secrets-config.json';
9
10 Future<void> main(List<String> args) async => generateSecretsConfig(args);
11
@@ -18,8 +19,10 @@ Future<void> generateSecretsConfig(List<String> args) async {
19
20 final configFile = File(configPath);
21 final evmChainsConfigFile = File(evmChainsConfigPath);
21 - final secrets = <String, dynamic>{};
22 + final solanaConfigFile = File(solanaConfigPath);
23
24 + final secrets = <String, dynamic>{};
25 +
26 secrets.addAll(extraInfo);
27 secrets.removeWhere((key, dynamic value) {
28 if (key.contains('--')) {
@@ -49,6 +52,7 @@ Future<void> generateSecretsConfig(List<String> args) async {
52 await configFile.writeAsString(secretsJson);
53
54 secrets.clear();
55 +
56 SecretKey.evmChainsSecrets.forEach((sec) {
57 if (secrets[sec.name] != null) {
58 return;
@@ -60,4 +64,18 @@ Future<void> generateSecretsConfig(List<String> args) async {
64 secretsJson = JsonEncoder.withIndent(' ').convert(secrets);
65
66 await evmChainsConfigFile.writeAsString(secretsJson);
67 +
68 + secrets.clear();
69 +
70 + SecretKey.solanaSecrets.forEach((sec) {
71 + if (secrets[sec.name] != null) {
72 + return;
73 + }
74 +
75 + secrets[sec.name] = sec.generate();
76 + });
77 +
78 + secretsJson = JsonEncoder.withIndent(' ').convert(secrets);
79 +
80 + await solanaConfigFile.writeAsString(secretsJson);
81 }
tool/import_secrets_config.dart
+14
@@ -8,6 +8,8 @@ const outputPath = 'lib/.secrets.g.dart';
8 const evmChainsConfigPath = 'tool/.evm-secrets-config.json';
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 Future<void> main(List<String> args) async => importSecretsConfig();
14
15 Future<void> importSecretsConfig() async {
@@ -21,6 +23,12 @@ Future<void> importSecretsConfig() async {
23 final evmChainsOutput = evmChainsInput.keys
24 .fold('', (String acc, String val) => acc + generateConst(val, evmChainsInput));
25
26 + final solanaOutputFile = File(solanaOutputPath);
27 + final solanaInput =
28 + json.decode(File(solanaConfigPath).readAsStringSync()) as Map<String, dynamic>;
29 + final solanaOutput =
30 + solanaInput.keys.fold('', (String acc, String val) => acc + generateConst(val, solanaInput));
31 +
32 if (outputFile.existsSync()) {
33 await outputFile.delete();
34 }
@@ -32,4 +40,10 @@ Future<void> importSecretsConfig() async {
40 }
41
42 await evmChainsOutputFile.writeAsString(evmChainsOutput);
43 +
44 + if (solanaOutputFile.existsSync()) {
45 + await solanaOutputFile.delete();
46 + }
47 +
48 + await solanaOutputFile.writeAsString(solanaOutput);
49 }
tool/utils/secret_key.dart
+4
@@ -44,6 +44,10 @@ class SecretKey {
44 SecretKey('polygonScanApiKey', () => ''),
45 ];
46
47 + static final solanaSecrets = [
48 + SecretKey('ankrApiKey', () => ''),
49 + ];
50 +
51 final String name;
52 final String Function() generate;
53 }