diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml
index 12d5dda5b..d8251a614 100644
--- a/.github/workflows/pr_test_build.yml
+++ b/.github/workflows/pr_test_build.yml
@@ -6,9 +6,9 @@ on:
workflow_dispatch:
inputs:
branch:
- description: 'Branch name to build'
+ description: "Branch name to build"
required: true
- default: 'main'
+ default: "main"
jobs:
PR_test_build:
@@ -112,6 +112,7 @@ jobs:
cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_lightning && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
+ cd cw_solana && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_ethereum && flutter pub get && cd ..
cd cw_polygon && flutter pub get && cd ..
flutter packages pub run build_runner build --delete-conflicting-outputs
@@ -121,6 +122,7 @@ jobs:
cd /opt/android/cake_wallet
touch lib/.secrets.g.dart
touch cw_evm/lib/.secrets.g.dart
+ touch cw_solana/lib/.secrets.g.dart
echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart
echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart
echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart
@@ -157,6 +159,7 @@ jobs:
echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
echo "const breezApiKey = '${{ secrets.BREEZ_API_KEY }}';" >> cw_lightning/lib/.secrets.g.dart
echo "const breezInviteCode = '${{ secrets.BREEZ_INVITE_CODE }}';" >> cw_lightning/lib/.secrets.g.dart
+ echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart
- name: Rename app
run: echo -e "id=com.cakewallet.test\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties
@@ -166,18 +169,18 @@ jobs:
cd /opt/android/cake_wallet
flutter build apk --release
-# - name: Push to App Center
-# run: |
-# echo 'Installing App Center CLI tools'
-# npm install -g appcenter-cli
-# echo "Publishing test to App Center"
-# appcenter distribute release \
-# --group "Testers" \
-# --file "/opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk" \
-# --release-notes ${{ env.BRANCH_NAME }} \
-# --app Cake-Labs/Cake-Wallet \
-# --token ${{ secrets.APP_CENTER_TOKEN }} \
-# --quiet
+ # - name: Push to App Center
+ # run: |
+ # echo 'Installing App Center CLI tools'
+ # npm install -g appcenter-cli
+ # echo "Publishing test to App Center"
+ # appcenter distribute release \
+ # --group "Testers" \
+ # --file "/opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk" \
+ # --release-notes ${{ env.BRANCH_NAME }} \
+ # --app Cake-Labs/Cake-Wallet \
+ # --token ${{ secrets.APP_CENTER_TOKEN }} \
+ # --quiet
- name: Rename apk file
run: |
diff --git a/.gitignore b/.gitignore
index c06b277f8..9300421f4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -92,8 +92,10 @@ android/key.properties
**/tool/.secrets-config.json
**/tool/.evm-secrets-config.json
**/tool/.ethereum-secrets-config.json
+**/tool/.solana-secrets-config.json
**/lib/.secrets.g.dart
**/cw_evm/lib/.secrets.g.dart
+**/cw_solana/lib/.secrets.g.dart
vendor/
@@ -129,6 +131,7 @@ lib/ethereum/ethereum.dart
lib/bitcoin_cash/bitcoin_cash.dart
lib/nano/nano.dart
lib/polygon/polygon.dart
+lib/solana/solana.dart
ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png
ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png
diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml
index 4929b5763..91bf93dbd 100644
--- a/android/app/src/main/AndroidManifestBase.xml
+++ b/android/app/src/main/AndroidManifestBase.xml
@@ -69,6 +69,7 @@
+
with Serializable implemen
required this.decimals,
this.fullName,
this.iconPath,
- this.tag})
+ this.tag, this.enabled = false,
+ })
: super(title: title, raw: raw);
final String name;
@@ -17,6 +18,9 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen
final String? fullName;
final String? iconPath;
final int decimals;
+ final bool enabled;
+
+ set enabled(bool value) => this.enabled = value;
static const all = [
CryptoCurrency.xmr,
@@ -208,6 +212,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen
static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POLY', fullName: 'Tether USD (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/usdt_icon.png', decimals: 6);
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);
static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kaspa', iconPath: 'assets/images/kaspa_icon.png', decimals: 8);
+ static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'USDT Tether', raw: 90, name: 'usdtsol', iconPath: 'assets/images/usdt_icon.png', decimals: 6);
static final Map _rawCurrencyMap =
diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart
index bcc34fd6f..9ca5dd2c1 100644
--- a/cw_core/lib/currency_for_wallet_type.dart
+++ b/cw_core/lib/currency_for_wallet_type.dart
@@ -23,6 +23,8 @@ CryptoCurrency currencyForWalletType(WalletType type) {
return CryptoCurrency.banano;
case WalletType.polygon:
return CryptoCurrency.maticpoly;
+ case WalletType.solana:
+ return CryptoCurrency.sol;
default:
throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType');
}
diff --git a/cw_core/lib/hive_type_ids.dart b/cw_core/lib/hive_type_ids.dart
index 4d4d1a6a8..3fa2eb647 100644
--- a/cw_core/lib/hive_type_ids.dart
+++ b/cw_core/lib/hive_type_ids.dart
@@ -13,4 +13,5 @@ const ADDRESS_INFO_TYPE_ID = 11;
const ERC20_TOKEN_TYPE_ID = 12;
const NANO_ACCOUNT_TYPE_ID = 13;
const POW_NODE_TYPE_ID = 14;
-const DERIVATION_TYPE_TYPE_ID = 15;
\ No newline at end of file
+const DERIVATION_TYPE_TYPE_ID = 15;
+const SPL_TOKEN_TYPE_ID = 16;
diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart
index 2c43dd21a..585bc3c38 100644
--- a/cw_core/lib/node.dart
+++ b/cw_core/lib/node.dart
@@ -70,15 +70,10 @@ class Node extends HiveObject with Keyable {
Uri get uri {
switch (type) {
case WalletType.monero:
- return Uri.http(uriRaw, '');
- case WalletType.bitcoin:
- return createUriFromElectrumAddress(uriRaw);
- case WalletType.litecoin:
- return createUriFromElectrumAddress(uriRaw);
case WalletType.haven:
return Uri.http(uriRaw, '');
- case WalletType.ethereum:
- return Uri.https(uriRaw, '');
+ case WalletType.bitcoin:
+ case WalletType.litecoin:
case WalletType.bitcoinCash:
return createUriFromElectrumAddress(uriRaw);
case WalletType.nano:
@@ -88,7 +83,9 @@ class Node extends HiveObject with Keyable {
} else {
return Uri.http(uriRaw, '');
}
+ case WalletType.ethereum:
case WalletType.polygon:
+ case WalletType.solana:
return Uri.https(uriRaw, '');
default:
throw Exception('Unexpected type ${type.toString()} for Node uri');
@@ -134,21 +131,17 @@ class Node extends HiveObject with Keyable {
try {
switch (type) {
case WalletType.monero:
- return requestMoneroNode();
- case WalletType.bitcoin:
- return requestElectrumServer();
- case WalletType.litecoin:
- return requestElectrumServer();
case WalletType.haven:
return requestMoneroNode();
- case WalletType.ethereum:
- return requestElectrumServer();
- case WalletType.bitcoinCash:
- return requestElectrumServer();
case WalletType.nano:
case WalletType.banano:
return requestNanoNode();
+ case WalletType.bitcoin:
+ case WalletType.litecoin:
+ case WalletType.bitcoinCash:
+ case WalletType.ethereum:
case WalletType.polygon:
+ case WalletType.solana:
return requestElectrumServer();
default:
return false;
diff --git a/cw_core/lib/pathForWallet.dart b/cw_core/lib/pathForWallet.dart
index af4838ffa..cfc33ef21 100644
--- a/cw_core/lib/pathForWallet.dart
+++ b/cw_core/lib/pathForWallet.dart
@@ -1,6 +1,5 @@
import 'dart:io';
import 'package:cw_core/wallet_type.dart';
-import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
Future pathForWalletDir({required String name, required WalletType type}) async {
diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart
index 5b58e9a56..edecc6d81 100644
--- a/cw_core/lib/wallet_type.dart
+++ b/cw_core/lib/wallet_type.dart
@@ -15,6 +15,7 @@ const walletTypes = [
WalletType.nano,
WalletType.banano,
WalletType.polygon,
+ WalletType.solana,
];
@HiveType(typeId: WALLET_TYPE_TYPE_ID)
@@ -50,6 +51,9 @@ enum WalletType {
polygon,
@HiveField(10)
+ solana,
+
+ @HiveField(11)
lightning
}
@@ -73,8 +77,10 @@ int serializeToInt(WalletType type) {
return 7;
case WalletType.polygon:
return 8;
- case WalletType.lightning:
+ case WalletType.solana:
return 9;
+ case WalletType.lightning:
+ return 10;
default:
return -1;
}
@@ -101,6 +107,8 @@ WalletType deserializeFromInt(int raw) {
case 8:
return WalletType.polygon;
case 9:
+ return WalletType.solana;
+ case 10:
return WalletType.lightning;
default:
throw Exception('Unexpected token: $raw for WalletType deserializeFromInt');
@@ -129,6 +137,8 @@ String walletTypeToString(WalletType type) {
return 'Polygon';
case WalletType.lightning:
return 'Lightning';
+ case WalletType.solana:
+ return 'Solana';
default:
return '';
}
@@ -156,6 +166,8 @@ String walletTypeToDisplayName(WalletType type) {
return 'Polygon (MATIC)';
case WalletType.lightning:
return 'Bitcoin (Lightning)';
+ case WalletType.solana:
+ return 'Solana (SOL)';
default:
return '';
}
@@ -183,6 +195,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) {
return CryptoCurrency.maticpoly;
case WalletType.lightning:
return CryptoCurrency.btc;
+ case WalletType.solana:
+ return CryptoCurrency.sol;
default:
throw Exception(
'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency');
diff --git a/cw_solana/.gitignore b/cw_solana/.gitignore
new file mode 100644
index 000000000..96486fd93
--- /dev/null
+++ b/cw_solana/.gitignore
@@ -0,0 +1,30 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
+/pubspec.lock
+**/doc/api/
+.dart_tool/
+.packages
+build/
diff --git a/cw_solana/.metadata b/cw_solana/.metadata
new file mode 100644
index 000000000..fa347fc6a
--- /dev/null
+++ b/cw_solana/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
+ channel: stable
+
+project_type: package
diff --git a/cw_solana/CHANGELOG.md b/cw_solana/CHANGELOG.md
new file mode 100644
index 000000000..41cc7d819
--- /dev/null
+++ b/cw_solana/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.0.1
+
+* TODO: Describe initial release.
diff --git a/cw_solana/LICENSE b/cw_solana/LICENSE
new file mode 100644
index 000000000..ba75c69f7
--- /dev/null
+++ b/cw_solana/LICENSE
@@ -0,0 +1 @@
+TODO: Add your license here.
diff --git a/cw_solana/README.md b/cw_solana/README.md
new file mode 100644
index 000000000..02fe8ecab
--- /dev/null
+++ b/cw_solana/README.md
@@ -0,0 +1,39 @@
+
+
+TODO: Put a short description of the package here that helps potential users
+know whether this package might be useful for them.
+
+## Features
+
+TODO: List what your package can do. Maybe include images, gifs, or videos.
+
+## Getting started
+
+TODO: List prerequisites and provide or point to information on how to
+start using the package.
+
+## Usage
+
+TODO: Include short and useful examples for package users. Add longer examples
+to `/example` folder.
+
+```dart
+const like = 'sample';
+```
+
+## Additional information
+
+TODO: Tell users more about the package: where to find more information, how to
+contribute to the package, how to file issues, what response they can expect
+from the package authors, and more.
diff --git a/cw_solana/analysis_options.yaml b/cw_solana/analysis_options.yaml
new file mode 100644
index 000000000..a5744c1cf
--- /dev/null
+++ b/cw_solana/analysis_options.yaml
@@ -0,0 +1,4 @@
+include: package:flutter_lints/flutter.yaml
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/cw_solana/lib/cw_solana.dart b/cw_solana/lib/cw_solana.dart
new file mode 100644
index 000000000..d04069b3b
--- /dev/null
+++ b/cw_solana/lib/cw_solana.dart
@@ -0,0 +1,7 @@
+library cw_solana;
+
+/// A Calculator.
+class Calculator {
+ /// Returns [value] plus 1.
+ int addOne(int value) => value + 1;
+}
diff --git a/cw_solana/lib/default_spl_tokens.dart b/cw_solana/lib/default_spl_tokens.dart
new file mode 100644
index 000000000..f96d62d86
--- /dev/null
+++ b/cw_solana/lib/default_spl_tokens.dart
@@ -0,0 +1,109 @@
+import 'package:cw_core/crypto_currency.dart';
+import 'package:cw_solana/spl_token.dart';
+
+class DefaultSPLTokens {
+ final List _defaultTokens = [
+ SPLToken(
+ name: 'USDT Tether',
+ symbol: 'USDT',
+ mintAddress: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
+ decimal: 6,
+ mint: 'usdtsol',
+ enabled: true,
+ ),
+ SPLToken(
+ name: 'USD Coin',
+ symbol: 'USDC',
+ mintAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
+ decimal: 6,
+ mint: 'usdcsol',
+ enabled: true,
+ ),
+ SPLToken(
+ name: 'Wrapped Ethereum (Sollet)',
+ symbol: 'soETH',
+ mintAddress: '2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk',
+ decimal: 6,
+ mint: 'soEth',
+ enabled: true,
+ iconPath: 'assets/images/eth_icon.png',
+ ),
+ SPLToken(
+ name: 'Wrapped SOL',
+ symbol: 'WSOL',
+ mintAddress: 'So11111111111111111111111111111111111111112',
+ decimal: 9,
+ mint: 'WSOL',
+ enabled: true,
+ iconPath: 'assets/images/sol_icon.png',
+ ),
+ SPLToken(
+ name: 'Wrapped Bitcoin (Sollet)',
+ symbol: 'BTC',
+ mintAddress: '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E',
+ decimal: 6,
+ mint: 'btcsol',
+ iconPath: 'assets/images/btc.png',
+ ),
+ SPLToken(
+ name: 'Bonk',
+ symbol: 'Bonk',
+ mintAddress: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263',
+ decimal: 5,
+ mint: 'Bonk',
+ iconPath: 'assets/images/bonk_icon.png',
+ ),
+ SPLToken(
+ name: 'Helium Network Token',
+ symbol: 'HNT',
+ mintAddress: 'hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux',
+ decimal: 8,
+ mint: 'hnt',
+ iconPath: 'assets/images/hnt_icon.png',
+ ),
+ SPLToken(
+ name: 'Pyth Network',
+ symbol: 'PYTH',
+ mintAddress: 'HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3',
+ decimal: 6,
+ mint: 'pyth',
+ ),
+ SPLToken(
+ name: 'Raydium',
+ symbol: 'RAY',
+ mintAddress: '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R',
+ decimal: 6,
+ mint: 'ray',
+ iconPath: 'assets/images/ray_icon.png',
+ ),
+ SPLToken(
+ name: 'GMT',
+ symbol: 'GMT',
+ mintAddress: '7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx',
+ decimal: 6,
+ mint: 'ray',
+ iconPath: 'assets/images/gmt_icon.png',
+ ),
+ SPLToken(
+ name: 'AvocadoCoin',
+ symbol: 'AVDO',
+ mintAddress: 'EE5L8cMU4itTsCSuor7NLK6RZx6JhsBe8GGV3oaAHm3P',
+ decimal: 8,
+ mint: 'avdo',
+ iconPath: 'assets/images/avdo_icon.png',
+ ),
+ ];
+
+ List get initialSPLTokens => _defaultTokens.map((token) {
+ String? iconPath;
+ if (token.iconPath != null) return token;
+
+ try {
+ iconPath = CryptoCurrency.all
+ .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
+ .iconPath;
+ } catch (_) {}
+
+ return SPLToken.copyWith(token, iconPath, 'SOL');
+ }).toList();
+}
diff --git a/cw_solana/lib/file.dart b/cw_solana/lib/file.dart
new file mode 100644
index 000000000..8fd236ec3
--- /dev/null
+++ b/cw_solana/lib/file.dart
@@ -0,0 +1,39 @@
+import 'dart:io';
+import 'package:cw_core/key.dart';
+import 'package:encrypt/encrypt.dart' as encrypt;
+
+Future write(
+ {required String path,
+ required String password,
+ required String data}) async {
+ final keys = extractKeys(password);
+ final key = encrypt.Key.fromBase64(keys.first);
+ final iv = encrypt.IV.fromBase64(keys.last);
+ final encrypted = await encode(key: key, iv: iv, data: data);
+ final f = File(path);
+ f.writeAsStringSync(encrypted);
+}
+
+Future writeData(
+ {required String path,
+ required String password,
+ required String data}) async {
+ final keys = extractKeys(password);
+ final key = encrypt.Key.fromBase64(keys.first);
+ final iv = encrypt.IV.fromBase64(keys.last);
+ final encrypted = await encode(key: key, iv: iv, data: data);
+ final f = File(path);
+ f.writeAsStringSync(encrypted);
+}
+
+Future read({required String path, required String password}) async {
+ final file = File(path);
+
+ if (!file.existsSync()) {
+ file.createSync();
+ }
+
+ final encrypted = file.readAsStringSync();
+
+ return decode(password: password, data: encrypted);
+}
diff --git a/cw_solana/lib/pending_solana_transaction.dart b/cw_solana/lib/pending_solana_transaction.dart
new file mode 100644
index 000000000..38347ed13
--- /dev/null
+++ b/cw_solana/lib/pending_solana_transaction.dart
@@ -0,0 +1,43 @@
+import 'package:cw_core/pending_transaction.dart';
+import 'package:solana/encoder.dart';
+
+class PendingSolanaTransaction with PendingTransaction {
+ final double amount;
+ final SignedTx signedTransaction;
+ final String destinationAddress;
+ final Function sendTransaction;
+ final double fee;
+
+ PendingSolanaTransaction({
+ required this.fee,
+ required this.amount,
+ required this.signedTransaction,
+ required this.destinationAddress,
+ required this.sendTransaction,
+ });
+
+ @override
+ String get amountFormatted {
+ String stringifiedAmount = amount.toString();
+
+ if (stringifiedAmount.toString().length >= 6) {
+ stringifiedAmount = stringifiedAmount.substring(0, 6);
+ }
+
+ return stringifiedAmount;
+ }
+
+ @override
+ Future commit() async {
+ return await sendTransaction();
+ }
+
+ @override
+ String get feeFormatted => fee.toString();
+
+ @override
+ String get hex => signedTransaction.encode();
+
+ @override
+ String get id => '';
+}
diff --git a/cw_solana/lib/solana_balance.dart b/cw_solana/lib/solana_balance.dart
new file mode 100644
index 000000000..b1f0ef153
--- /dev/null
+++ b/cw_solana/lib/solana_balance.dart
@@ -0,0 +1,39 @@
+import 'dart:convert';
+
+import 'package:cw_core/balance.dart';
+
+class SolanaBalance extends Balance {
+ SolanaBalance(this.balance) : super(balance.toInt(), balance.toInt());
+
+ final double balance;
+
+ @override
+ String get formattedAdditionalBalance => _balanceFormatted();
+
+ @override
+ String get formattedAvailableBalance => _balanceFormatted();
+
+ String _balanceFormatted() {
+ String stringBalance = balance.toString();
+ if (stringBalance.toString().length >= 6) {
+ stringBalance = stringBalance.substring(0, 6);
+ }
+ return stringBalance;
+ }
+
+ static SolanaBalance? fromJSON(String? jsonSource) {
+ if (jsonSource == null) {
+ return null;
+ }
+
+ final decoded = json.decode(jsonSource) as Map;
+
+ try {
+ return SolanaBalance(decoded['balance']);
+ } catch (e) {
+ return SolanaBalance(0.0);
+ }
+ }
+
+ String toJSON() => json.encode({'balance': balance.toString()});
+}
diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart
new file mode 100644
index 000000000..ececc56ba
--- /dev/null
+++ b/cw_solana/lib/solana_client.dart
@@ -0,0 +1,477 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:math';
+
+import 'package:cw_core/crypto_currency.dart';
+import 'package:cw_core/node.dart';
+import 'package:cw_solana/pending_solana_transaction.dart';
+import 'package:cw_solana/solana_balance.dart';
+import 'package:cw_solana/solana_transaction_model.dart';
+import 'package:http/http.dart' as http;
+import 'package:solana/dto.dart';
+import 'package:solana/encoder.dart';
+import 'package:solana/solana.dart';
+import '.secrets.g.dart' as secrets;
+
+class SolanaWalletClient {
+ final httpClient = http.Client();
+ SolanaClient? _client;
+
+ bool connect(Node node) {
+ try {
+ Uri? rpcUri;
+ String webSocketUrl;
+ bool isModifiedNodeUri = false;
+
+ if (node.uriRaw == 'rpc.ankr.com') {
+ isModifiedNodeUri = true;
+ String ankrApiKey = secrets.ankrApiKey;
+
+ rpcUri = Uri.https(node.uriRaw, '/solana/$ankrApiKey');
+ webSocketUrl = 'wss://${node.uriRaw}/solana/ws/$ankrApiKey';
+ } else {
+ webSocketUrl = 'wss://${node.uriRaw}';
+ }
+
+ _client = SolanaClient(
+ rpcUrl: isModifiedNodeUri ? rpcUri! : node.uri,
+ websocketUrl: Uri.parse(webSocketUrl),
+ timeout: const Duration(minutes: 2),
+ );
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ Future getBalance(String address) async {
+ try {
+ final balance = await _client!.rpcClient.getBalance(address);
+
+ final solBalance = balance.value / lamportsPerSol;
+
+ return solBalance;
+ } catch (_) {
+ return 0.0;
+ }
+ }
+
+ Future getSPLTokenAccounts(String mintAddress, String publicKey) async {
+ try {
+ final tokenAccounts = await _client!.rpcClient.getTokenAccountsByOwner(
+ publicKey,
+ TokenAccountsFilter.byMint(mintAddress),
+ commitment: Commitment.confirmed,
+ encoding: Encoding.jsonParsed,
+ );
+ return tokenAccounts;
+ } catch (e) {
+ return null;
+ }
+ }
+
+ Future getSplTokenBalance(String mintAddress, String publicKey) async {
+ // Fetch the token accounts (a token can have multiple accounts for various uses)
+ final tokenAccounts = await getSPLTokenAccounts(mintAddress, publicKey);
+
+ // Handle scenario where there is no token account
+ if (tokenAccounts == null || tokenAccounts.value.isEmpty) {
+ return null;
+ }
+
+ // Sum the balances of all accounts with the specified mint address
+ double totalBalance = 0.0;
+
+ for (var programAccount in tokenAccounts.value) {
+ final tokenAmountResult =
+ await _client!.rpcClient.getTokenAccountBalance(programAccount.pubkey);
+
+ final balance = tokenAmountResult.value.uiAmountString;
+
+ final balanceAsDouble = double.tryParse(balance ?? '0.0') ?? 0.0;
+
+ totalBalance += balanceAsDouble;
+ }
+
+ return SolanaBalance(totalBalance);
+ }
+
+ Future getGasForMessage(String message) async {
+ try {
+ final gasPrice = await _client!.rpcClient.getFeeForMessage(message) ?? 0;
+ final fee = gasPrice / lamportsPerSol;
+ return fee;
+ } catch (_) {
+ return 0;
+ }
+ }
+
+ /// Load the Address's transactions into the account
+ Future> fetchTransactions(
+ Ed25519HDPublicKey publicKey, {
+ String? splTokenSymbol,
+ int? splTokenDecimal,
+ }) async {
+ List transactions = [];
+
+ try {
+ final response = await _client!.rpcClient.getTransactionsList(
+ publicKey,
+ commitment: Commitment.confirmed,
+ limit: 1000,
+ );
+
+ for (final tx in response) {
+ if (tx.transaction is ParsedTransaction) {
+ final parsedTx = (tx.transaction as ParsedTransaction);
+ final message = parsedTx.message;
+
+ final fee = (tx.meta?.fee ?? 0) / lamportsPerSol;
+
+ for (final instruction in message.instructions) {
+ if (instruction is ParsedInstruction) {
+ instruction.map(
+ system: (systemData) {
+ systemData.parsed.map(
+ transfer: (transferData) {
+ ParsedSystemTransferInformation transfer = transferData.info;
+ bool isOutgoingTx = transfer.source == publicKey.toBase58();
+
+ double amount = transfer.lamports.toDouble() / lamportsPerSol;
+
+ transactions.add(
+ SolanaTransactionModel(
+ id: parsedTx.signatures.first,
+ from: transfer.source,
+ to: transfer.destination,
+ amount: amount,
+ isOutgoingTx: isOutgoingTx,
+ blockTimeInInt: tx.blockTime!,
+ fee: fee,
+ programId: SystemProgram.programId,
+ tokenSymbol: 'SOL',
+ ),
+ );
+ },
+ transferChecked: (_) {},
+ unsupported: (_) {},
+ );
+ },
+ splToken: (splTokenData) {
+ if (splTokenSymbol != null) {
+ splTokenData.parsed.map(
+ transfer: (transferData) {
+ SplTokenTransferInfo transfer = transferData.info;
+ bool isOutgoingTx = transfer.source == publicKey.toBase58();
+
+ double amount = (double.tryParse(transfer.amount) ?? 0.0) /
+ pow(10, splTokenDecimal ?? 9);
+
+ transactions.add(
+ SolanaTransactionModel(
+ id: parsedTx.signatures.first,
+ fee: fee,
+ from: transfer.source,
+ to: transfer.destination,
+ amount: amount,
+ isOutgoingTx: isOutgoingTx,
+ programId: TokenProgram.programId,
+ blockTimeInInt: tx.blockTime!,
+ tokenSymbol: splTokenSymbol,
+ ),
+ );
+ },
+ transferChecked: (transferCheckedData) {
+ SplTokenTransferCheckedInfo transfer = transferCheckedData.info;
+ bool isOutgoingTx = transfer.source == publicKey.toBase58();
+ double amount =
+ double.tryParse(transfer.tokenAmount.uiAmountString ?? '0.0') ?? 0.0;
+
+ transactions.add(
+ SolanaTransactionModel(
+ id: parsedTx.signatures.first,
+ fee: fee,
+ from: transfer.source,
+ to: transfer.destination,
+ amount: amount,
+ isOutgoingTx: isOutgoingTx,
+ programId: TokenProgram.programId,
+ blockTimeInInt: tx.blockTime!,
+ tokenSymbol: splTokenSymbol,
+ ),
+ );
+ },
+ generic: (genericData) {},
+ );
+ }
+ },
+ memo: (_) {},
+ unsupported: (a) {},
+ );
+ }
+ }
+ }
+ }
+
+ return transactions;
+ } catch (err) {
+ return [];
+ }
+ }
+
+ Future> getSPLTokenTransfers(
+ String address,
+ String splTokenSymbol,
+ int splTokenDecimal,
+ Ed25519HDKeyPair ownerKeypair,
+ ) async {
+ final tokenMint = Ed25519HDPublicKey.fromBase58(address);
+
+ ProgramAccount? associatedTokenAccount;
+
+ try {
+ associatedTokenAccount = await _client!.getAssociatedTokenAccount(
+ mint: tokenMint,
+ owner: ownerKeypair.publicKey,
+ commitment: Commitment.confirmed,
+ );
+ } catch (_) {}
+
+ if (associatedTokenAccount == null) return [];
+
+ final accountPublicKey = Ed25519HDPublicKey.fromBase58(associatedTokenAccount.pubkey);
+
+ final tokenTransactions = await fetchTransactions(
+ accountPublicKey,
+ splTokenSymbol: splTokenSymbol,
+ splTokenDecimal: splTokenDecimal,
+ );
+
+ return tokenTransactions;
+ }
+
+ void stop() {}
+
+ SolanaClient? get getSolanaClient => _client;
+
+ Future signSolanaTransaction({
+ required String tokenTitle,
+ required int tokenDecimals,
+ String? tokenMint,
+ required double inputAmount,
+ required String destinationAddress,
+ required Ed25519HDKeyPair ownerKeypair,
+ List references = const [],
+ }) async {
+ const commitment = Commitment.finalized;
+
+ final latestBlockhash =
+ await _client!.rpcClient.getLatestBlockhash(commitment: commitment).value;
+
+ final recentBlockhash = RecentBlockhash(
+ blockhash: latestBlockhash.blockhash,
+ feeCalculator: const FeeCalculator(
+ lamportsPerSignature: 500,
+ ),
+ );
+
+ if (tokenTitle == CryptoCurrency.sol.title) {
+ final pendingNativeTokenTransaction = await _signNativeTokenTransaction(
+ tokenTitle: tokenTitle,
+ tokenDecimals: tokenDecimals,
+ inputAmount: inputAmount,
+ destinationAddress: destinationAddress,
+ ownerKeypair: ownerKeypair,
+ recentBlockhash: recentBlockhash,
+ commitment: commitment,
+ );
+ return pendingNativeTokenTransaction;
+ } else {
+ final pendingSPLTokenTransaction = _signSPLTokenTransaction(
+ tokenTitle: tokenTitle,
+ tokenDecimals: tokenDecimals,
+ tokenMint: tokenMint!,
+ inputAmount: inputAmount,
+ destinationAddress: destinationAddress,
+ ownerKeypair: ownerKeypair,
+ recentBlockhash: recentBlockhash,
+ commitment: commitment,
+ );
+ return pendingSPLTokenTransaction;
+ }
+ }
+
+ Future _signNativeTokenTransaction({
+ required String tokenTitle,
+ required int tokenDecimals,
+ required double inputAmount,
+ required String destinationAddress,
+ required Ed25519HDKeyPair ownerKeypair,
+ required RecentBlockhash recentBlockhash,
+ required Commitment commitment,
+ }) async {
+ // Convert SOL to lamport
+ int lamports = (inputAmount * lamportsPerSol).toInt();
+
+ final instructions = [
+ SystemInstruction.transfer(
+ fundingAccount: ownerKeypair.publicKey,
+ recipientAccount: Ed25519HDPublicKey.fromBase58(destinationAddress),
+ lamports: lamports,
+ ),
+ ];
+
+ final message = Message(instructions: instructions);
+ final signers = [ownerKeypair];
+
+ final signedTx = await _signTransactionInternal(
+ message: message,
+ signers: signers,
+ commitment: commitment,
+ recentBlockhash: recentBlockhash,
+ );
+
+ final fee = await _getFeeFromCompiledMessage(
+ message,
+ recentBlockhash,
+ signers.first.publicKey,
+ );
+
+ sendTx() async => await sendTransaction(
+ signedTransaction: signedTx,
+ commitment: commitment,
+ );
+
+ final pendingTransaction = PendingSolanaTransaction(
+ amount: inputAmount,
+ signedTransaction: signedTx,
+ destinationAddress: destinationAddress,
+ sendTransaction: sendTx,
+ fee: fee,
+ );
+
+ return pendingTransaction;
+ }
+
+ Future _signSPLTokenTransaction({
+ required String tokenTitle,
+ required int tokenDecimals,
+ required String tokenMint,
+ required double inputAmount,
+ required String destinationAddress,
+ required Ed25519HDKeyPair ownerKeypair,
+ required RecentBlockhash recentBlockhash,
+ required Commitment commitment,
+ }) async {
+ final destinationOwner = Ed25519HDPublicKey.fromBase58(destinationAddress);
+ final mint = Ed25519HDPublicKey.fromBase58(tokenMint);
+
+ ProgramAccount? associatedRecipientAccount;
+ ProgramAccount? associatedSenderAccount;
+
+ associatedRecipientAccount = await _client!.getAssociatedTokenAccount(
+ mint: mint,
+ owner: destinationOwner,
+ commitment: commitment,
+ );
+
+ associatedSenderAccount = await _client!.getAssociatedTokenAccount(
+ owner: ownerKeypair.publicKey,
+ mint: mint,
+ commitment: commitment,
+ );
+
+ // Throw an appropriate exception if the sender has no associated
+ // token account
+ if (associatedSenderAccount == null) {
+ throw NoAssociatedTokenAccountException(ownerKeypair.address, mint.toBase58());
+ }
+
+ try {
+ associatedRecipientAccount ??= await _client!.createAssociatedTokenAccount(
+ mint: mint,
+ owner: destinationOwner,
+ funder: ownerKeypair,
+ );
+ } catch (e) {
+ throw Exception(
+ 'Error while creating an associated token account for the recipient: ${e.toString()}',
+ );
+ }
+
+ // Input by the user
+ final amount = (inputAmount * pow(10, tokenDecimals)).toInt();
+
+ final instruction = TokenInstruction.transfer(
+ source: Ed25519HDPublicKey.fromBase58(associatedSenderAccount.pubkey),
+ destination: Ed25519HDPublicKey.fromBase58(associatedRecipientAccount.pubkey),
+ owner: ownerKeypair.publicKey,
+ amount: amount,
+ );
+
+ final message = Message(instructions: [instruction]);
+ final signers = [ownerKeypair];
+
+ final signedTx = await _signTransactionInternal(
+ message: message,
+ signers: signers,
+ commitment: commitment,
+ recentBlockhash: recentBlockhash,
+ );
+
+ final fee = await _getFeeFromCompiledMessage(
+ message,
+ recentBlockhash,
+ signers.first.publicKey,
+ );
+
+ sendTx() async => await sendTransaction(
+ signedTransaction: signedTx,
+ commitment: commitment,
+ );
+
+ final pendingTransaction = PendingSolanaTransaction(
+ amount: inputAmount,
+ signedTransaction: signedTx,
+ destinationAddress: destinationAddress,
+ sendTransaction: sendTx,
+ fee: fee,
+ );
+ return pendingTransaction;
+ }
+
+ Future _getFeeFromCompiledMessage(
+ Message message, RecentBlockhash recentBlockhash, Ed25519HDPublicKey feePayer) async {
+ final compile = message.compile(
+ recentBlockhash: recentBlockhash.blockhash,
+ feePayer: feePayer,
+ );
+
+ final base64Message = base64Encode(compile.toByteArray().toList());
+
+ final fee = await getGasForMessage(base64Message);
+ return fee;
+ }
+
+ Future _signTransactionInternal({
+ required Message message,
+ required List signers,
+ required Commitment commitment,
+ required RecentBlockhash recentBlockhash,
+ }) async {
+ final signedTx = await signTransaction(recentBlockhash, message, signers);
+
+ return signedTx;
+ }
+
+ Future sendTransaction({
+ required SignedTx signedTransaction,
+ required Commitment commitment,
+ }) async {
+ final signature = await _client!.rpcClient.sendTransaction(signedTransaction.encode());
+
+ _client!.waitForSignatureStatus(signature, status: commitment);
+
+ return signature;
+ }
+}
diff --git a/cw_solana/lib/solana_exceptions.dart b/cw_solana/lib/solana_exceptions.dart
new file mode 100644
index 000000000..7409b0500
--- /dev/null
+++ b/cw_solana/lib/solana_exceptions.dart
@@ -0,0 +1,21 @@
+import 'package:cw_core/crypto_currency.dart';
+
+class SolanaTransactionCreationException implements Exception {
+ final String exceptionMessage;
+
+ SolanaTransactionCreationException(CryptoCurrency currency)
+ : exceptionMessage = 'Error creating ${currency.title} transaction.';
+
+ @override
+ String toString() => exceptionMessage;
+}
+
+class SolanaTransactionWrongBalanceException implements Exception {
+ final String exceptionMessage;
+
+ SolanaTransactionWrongBalanceException(CryptoCurrency currency)
+ : exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.';
+
+ @override
+ String toString() => exceptionMessage;
+}
diff --git a/cw_solana/lib/solana_mnemonics.dart b/cw_solana/lib/solana_mnemonics.dart
new file mode 100644
index 000000000..21cbb613a
--- /dev/null
+++ b/cw_solana/lib/solana_mnemonics.dart
@@ -0,0 +1,2058 @@
+class SolanaMnemonicIsIncorrectException implements Exception {
+ @override
+ String toString() =>
+ 'Solana mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.';
+}
+
+class SolanaMnemonics {
+ static const englishWordlist = [
+ 'abandon',
+ 'ability',
+ 'able',
+ 'about',
+ 'above',
+ 'absent',
+ 'absorb',
+ 'abstract',
+ 'absurd',
+ 'abuse',
+ 'access',
+ 'accident',
+ 'account',
+ 'accuse',
+ 'achieve',
+ 'acid',
+ 'acoustic',
+ 'acquire',
+ 'across',
+ 'act',
+ 'action',
+ 'actor',
+ 'actress',
+ 'actual',
+ 'adapt',
+ 'add',
+ 'addict',
+ 'address',
+ 'adjust',
+ 'admit',
+ 'adult',
+ 'advance',
+ 'advice',
+ 'aerobic',
+ 'affair',
+ 'afford',
+ 'afraid',
+ 'again',
+ 'age',
+ 'agent',
+ 'agree',
+ 'ahead',
+ 'aim',
+ 'air',
+ 'airport',
+ 'aisle',
+ 'alarm',
+ 'album',
+ 'alcohol',
+ 'alert',
+ 'alien',
+ 'all',
+ 'alley',
+ 'allow',
+ 'almost',
+ 'alone',
+ 'alpha',
+ 'already',
+ 'also',
+ 'alter',
+ 'always',
+ 'amateur',
+ 'amazing',
+ 'among',
+ 'amount',
+ 'amused',
+ 'analyst',
+ 'anchor',
+ 'ancient',
+ 'anger',
+ 'angle',
+ 'angry',
+ 'animal',
+ 'ankle',
+ 'announce',
+ 'annual',
+ 'another',
+ 'answer',
+ 'antenna',
+ 'antique',
+ 'anxiety',
+ 'any',
+ 'apart',
+ 'apology',
+ 'appear',
+ 'apple',
+ 'approve',
+ 'april',
+ 'arch',
+ 'arctic',
+ 'area',
+ 'arena',
+ 'argue',
+ 'arm',
+ 'armed',
+ 'armor',
+ 'army',
+ 'around',
+ 'arrange',
+ 'arrest',
+ 'arrive',
+ 'arrow',
+ 'art',
+ 'artefact',
+ 'artist',
+ 'artwork',
+ 'ask',
+ 'aspect',
+ 'assault',
+ 'asset',
+ 'assist',
+ 'assume',
+ 'asthma',
+ 'athlete',
+ 'atom',
+ 'attack',
+ 'attend',
+ 'attitude',
+ 'attract',
+ 'auction',
+ 'audit',
+ 'august',
+ 'aunt',
+ 'author',
+ 'auto',
+ 'autumn',
+ 'average',
+ 'avocado',
+ 'avoid',
+ 'awake',
+ 'aware',
+ 'away',
+ 'awesome',
+ 'awful',
+ 'awkward',
+ 'axis',
+ 'baby',
+ 'bachelor',
+ 'bacon',
+ 'badge',
+ 'bag',
+ 'balance',
+ 'balcony',
+ 'ball',
+ 'bamboo',
+ 'banana',
+ 'banner',
+ 'bar',
+ 'barely',
+ 'bargain',
+ 'barrel',
+ 'base',
+ 'basic',
+ 'basket',
+ 'battle',
+ 'beach',
+ 'bean',
+ 'beauty',
+ 'because',
+ 'become',
+ 'beef',
+ 'before',
+ 'begin',
+ 'behave',
+ 'behind',
+ 'believe',
+ 'below',
+ 'belt',
+ 'bench',
+ 'benefit',
+ 'best',
+ 'betray',
+ 'better',
+ 'between',
+ 'beyond',
+ 'bicycle',
+ 'bid',
+ 'bike',
+ 'bind',
+ 'biology',
+ 'bird',
+ 'birth',
+ 'bitter',
+ 'black',
+ 'blade',
+ 'blame',
+ 'blanket',
+ 'blast',
+ 'bleak',
+ 'bless',
+ 'blind',
+ 'blood',
+ 'blossom',
+ 'blouse',
+ 'blue',
+ 'blur',
+ 'blush',
+ 'board',
+ 'boat',
+ 'body',
+ 'boil',
+ 'bomb',
+ 'bone',
+ 'bonus',
+ 'book',
+ 'boost',
+ 'border',
+ 'boring',
+ 'borrow',
+ 'boss',
+ 'bottom',
+ 'bounce',
+ 'box',
+ 'boy',
+ 'bracket',
+ 'brain',
+ 'brand',
+ 'brass',
+ 'brave',
+ 'bread',
+ 'breeze',
+ 'brick',
+ 'bridge',
+ 'brief',
+ 'bright',
+ 'bring',
+ 'brisk',
+ 'broccoli',
+ 'broken',
+ 'bronze',
+ 'broom',
+ 'brother',
+ 'brown',
+ 'brush',
+ 'bubble',
+ 'buddy',
+ 'budget',
+ 'buffalo',
+ 'build',
+ 'bulb',
+ 'bulk',
+ 'bullet',
+ 'bundle',
+ 'bunker',
+ 'burden',
+ 'burger',
+ 'burst',
+ 'bus',
+ 'business',
+ 'busy',
+ 'butter',
+ 'buyer',
+ 'buzz',
+ 'cabbage',
+ 'cabin',
+ 'cable',
+ 'cactus',
+ 'cage',
+ 'cake',
+ 'call',
+ 'calm',
+ 'camera',
+ 'camp',
+ 'can',
+ 'canal',
+ 'cancel',
+ 'candy',
+ 'cannon',
+ 'canoe',
+ 'canvas',
+ 'canyon',
+ 'capable',
+ 'capital',
+ 'captain',
+ 'car',
+ 'carbon',
+ 'card',
+ 'cargo',
+ 'carpet',
+ 'carry',
+ 'cart',
+ 'case',
+ 'cash',
+ 'casino',
+ 'castle',
+ 'casual',
+ 'cat',
+ 'catalog',
+ 'catch',
+ 'category',
+ 'cattle',
+ 'caught',
+ 'cause',
+ 'caution',
+ 'cave',
+ 'ceiling',
+ 'celery',
+ 'cement',
+ 'census',
+ 'century',
+ 'cereal',
+ 'certain',
+ 'chair',
+ 'chalk',
+ 'champion',
+ 'change',
+ 'chaos',
+ 'chapter',
+ 'charge',
+ 'chase',
+ 'chat',
+ 'cheap',
+ 'check',
+ 'cheese',
+ 'chef',
+ 'cherry',
+ 'chest',
+ 'chicken',
+ 'chief',
+ 'child',
+ 'chimney',
+ 'choice',
+ 'choose',
+ 'chronic',
+ 'chuckle',
+ 'chunk',
+ 'churn',
+ 'cigar',
+ 'cinnamon',
+ 'circle',
+ 'citizen',
+ 'city',
+ 'civil',
+ 'claim',
+ 'clap',
+ 'clarify',
+ 'claw',
+ 'clay',
+ 'clean',
+ 'clerk',
+ 'clever',
+ 'click',
+ 'client',
+ 'cliff',
+ 'climb',
+ 'clinic',
+ 'clip',
+ 'clock',
+ 'clog',
+ 'close',
+ 'cloth',
+ 'cloud',
+ 'clown',
+ 'club',
+ 'clump',
+ 'cluster',
+ 'clutch',
+ 'coach',
+ 'coast',
+ 'coconut',
+ 'code',
+ 'coffee',
+ 'coil',
+ 'coin',
+ 'collect',
+ 'color',
+ 'column',
+ 'combine',
+ 'come',
+ 'comfort',
+ 'comic',
+ 'common',
+ 'company',
+ 'concert',
+ 'conduct',
+ 'confirm',
+ 'congress',
+ 'connect',
+ 'consider',
+ 'control',
+ 'convince',
+ 'cook',
+ 'cool',
+ 'copper',
+ 'copy',
+ 'coral',
+ 'core',
+ 'corn',
+ 'correct',
+ 'cost',
+ 'cotton',
+ 'couch',
+ 'country',
+ 'couple',
+ 'course',
+ 'cousin',
+ 'cover',
+ 'coyote',
+ 'crack',
+ 'cradle',
+ 'craft',
+ 'cram',
+ 'crane',
+ 'crash',
+ 'crater',
+ 'crawl',
+ 'crazy',
+ 'cream',
+ 'credit',
+ 'creek',
+ 'crew',
+ 'cricket',
+ 'crime',
+ 'crisp',
+ 'critic',
+ 'crop',
+ 'cross',
+ 'crouch',
+ 'crowd',
+ 'crucial',
+ 'cruel',
+ 'cruise',
+ 'crumble',
+ 'crunch',
+ 'crush',
+ 'cry',
+ 'crystal',
+ 'cube',
+ 'culture',
+ 'cup',
+ 'cupboard',
+ 'curious',
+ 'current',
+ 'curtain',
+ 'curve',
+ 'cushion',
+ 'custom',
+ 'cute',
+ 'cycle',
+ 'dad',
+ 'damage',
+ 'damp',
+ 'dance',
+ 'danger',
+ 'daring',
+ 'dash',
+ 'daughter',
+ 'dawn',
+ 'day',
+ 'deal',
+ 'debate',
+ 'debris',
+ 'decade',
+ 'december',
+ 'decide',
+ 'decline',
+ 'decorate',
+ 'decrease',
+ 'deer',
+ 'defense',
+ 'define',
+ 'defy',
+ 'degree',
+ 'delay',
+ 'deliver',
+ 'demand',
+ 'demise',
+ 'denial',
+ 'dentist',
+ 'deny',
+ 'depart',
+ 'depend',
+ 'deposit',
+ 'depth',
+ 'deputy',
+ 'derive',
+ 'describe',
+ 'desert',
+ 'design',
+ 'desk',
+ 'despair',
+ 'destroy',
+ 'detail',
+ 'detect',
+ 'develop',
+ 'device',
+ 'devote',
+ 'diagram',
+ 'dial',
+ 'diamond',
+ 'diary',
+ 'dice',
+ 'diesel',
+ 'diet',
+ 'differ',
+ 'digital',
+ 'dignity',
+ 'dilemma',
+ 'dinner',
+ 'dinosaur',
+ 'direct',
+ 'dirt',
+ 'disagree',
+ 'discover',
+ 'disease',
+ 'dish',
+ 'dismiss',
+ 'disorder',
+ 'display',
+ 'distance',
+ 'divert',
+ 'divide',
+ 'divorce',
+ 'dizzy',
+ 'doctor',
+ 'document',
+ 'dog',
+ 'doll',
+ 'dolphin',
+ 'domain',
+ 'donate',
+ 'donkey',
+ 'donor',
+ 'door',
+ 'dose',
+ 'double',
+ 'dove',
+ 'draft',
+ 'dragon',
+ 'drama',
+ 'drastic',
+ 'draw',
+ 'dream',
+ 'dress',
+ 'drift',
+ 'drill',
+ 'drink',
+ 'drip',
+ 'drive',
+ 'drop',
+ 'drum',
+ 'dry',
+ 'duck',
+ 'dumb',
+ 'dune',
+ 'during',
+ 'dust',
+ 'dutch',
+ 'duty',
+ 'dwarf',
+ 'dynamic',
+ 'eager',
+ 'eagle',
+ 'early',
+ 'earn',
+ 'earth',
+ 'easily',
+ 'east',
+ 'easy',
+ 'echo',
+ 'ecology',
+ 'economy',
+ 'edge',
+ 'edit',
+ 'educate',
+ 'effort',
+ 'egg',
+ 'eight',
+ 'either',
+ 'elbow',
+ 'elder',
+ 'electric',
+ 'elegant',
+ 'element',
+ 'elephant',
+ 'elevator',
+ 'elite',
+ 'else',
+ 'embark',
+ 'embody',
+ 'embrace',
+ 'emerge',
+ 'emotion',
+ 'employ',
+ 'empower',
+ 'empty',
+ 'enable',
+ 'enact',
+ 'end',
+ 'endless',
+ 'endorse',
+ 'enemy',
+ 'energy',
+ 'enforce',
+ 'engage',
+ 'engine',
+ 'enhance',
+ 'enjoy',
+ 'enlist',
+ 'enough',
+ 'enrich',
+ 'enroll',
+ 'ensure',
+ 'enter',
+ 'entire',
+ 'entry',
+ 'envelope',
+ 'episode',
+ 'equal',
+ 'equip',
+ 'era',
+ 'erase',
+ 'erode',
+ 'erosion',
+ 'error',
+ 'erupt',
+ 'escape',
+ 'essay',
+ 'essence',
+ 'estate',
+ 'eternal',
+ 'ethics',
+ 'evidence',
+ 'evil',
+ 'evoke',
+ 'evolve',
+ 'exact',
+ 'example',
+ 'excess',
+ 'exchange',
+ 'excite',
+ 'exclude',
+ 'excuse',
+ 'execute',
+ 'exercise',
+ 'exhaust',
+ 'exhibit',
+ 'exile',
+ 'exist',
+ 'exit',
+ 'exotic',
+ 'expand',
+ 'expect',
+ 'expire',
+ 'explain',
+ 'expose',
+ 'express',
+ 'extend',
+ 'extra',
+ 'eye',
+ 'eyebrow',
+ 'fabric',
+ 'face',
+ 'faculty',
+ 'fade',
+ 'faint',
+ 'faith',
+ 'fall',
+ 'false',
+ 'fame',
+ 'family',
+ 'famous',
+ 'fan',
+ 'fancy',
+ 'fantasy',
+ 'farm',
+ 'fashion',
+ 'fat',
+ 'fatal',
+ 'father',
+ 'fatigue',
+ 'fault',
+ 'favorite',
+ 'feature',
+ 'february',
+ 'federal',
+ 'fee',
+ 'feed',
+ 'feel',
+ 'female',
+ 'fence',
+ 'festival',
+ 'fetch',
+ 'fever',
+ 'few',
+ 'fiber',
+ 'fiction',
+ 'field',
+ 'figure',
+ 'file',
+ 'film',
+ 'filter',
+ 'final',
+ 'find',
+ 'fine',
+ 'finger',
+ 'finish',
+ 'fire',
+ 'firm',
+ 'first',
+ 'fiscal',
+ 'fish',
+ 'fit',
+ 'fitness',
+ 'fix',
+ 'flag',
+ 'flame',
+ 'flash',
+ 'flat',
+ 'flavor',
+ 'flee',
+ 'flight',
+ 'flip',
+ 'float',
+ 'flock',
+ 'floor',
+ 'flower',
+ 'fluid',
+ 'flush',
+ 'fly',
+ 'foam',
+ 'focus',
+ 'fog',
+ 'foil',
+ 'fold',
+ 'follow',
+ 'food',
+ 'foot',
+ 'force',
+ 'forest',
+ 'forget',
+ 'fork',
+ 'fortune',
+ 'forum',
+ 'forward',
+ 'fossil',
+ 'foster',
+ 'found',
+ 'fox',
+ 'fragile',
+ 'frame',
+ 'frequent',
+ 'fresh',
+ 'friend',
+ 'fringe',
+ 'frog',
+ 'front',
+ 'frost',
+ 'frown',
+ 'frozen',
+ 'fruit',
+ 'fuel',
+ 'fun',
+ 'funny',
+ 'furnace',
+ 'fury',
+ 'future',
+ 'gadget',
+ 'gain',
+ 'galaxy',
+ 'gallery',
+ 'game',
+ 'gap',
+ 'garage',
+ 'garbage',
+ 'garden',
+ 'garlic',
+ 'garment',
+ 'gas',
+ 'gasp',
+ 'gate',
+ 'gather',
+ 'gauge',
+ 'gaze',
+ 'general',
+ 'genius',
+ 'genre',
+ 'gentle',
+ 'genuine',
+ 'gesture',
+ 'ghost',
+ 'giant',
+ 'gift',
+ 'giggle',
+ 'ginger',
+ 'giraffe',
+ 'girl',
+ 'give',
+ 'glad',
+ 'glance',
+ 'glare',
+ 'glass',
+ 'glide',
+ 'glimpse',
+ 'globe',
+ 'gloom',
+ 'glory',
+ 'glove',
+ 'glow',
+ 'glue',
+ 'goat',
+ 'goddess',
+ 'gold',
+ 'good',
+ 'goose',
+ 'gorilla',
+ 'gospel',
+ 'gossip',
+ 'govern',
+ 'gown',
+ 'grab',
+ 'grace',
+ 'grain',
+ 'grant',
+ 'grape',
+ 'grass',
+ 'gravity',
+ 'great',
+ 'green',
+ 'grid',
+ 'grief',
+ 'grit',
+ 'grocery',
+ 'group',
+ 'grow',
+ 'grunt',
+ 'guard',
+ 'guess',
+ 'guide',
+ 'guilt',
+ 'guitar',
+ 'gun',
+ 'gym',
+ 'habit',
+ 'hair',
+ 'half',
+ 'hammer',
+ 'hamster',
+ 'hand',
+ 'happy',
+ 'harbor',
+ 'hard',
+ 'harsh',
+ 'harvest',
+ 'hat',
+ 'have',
+ 'hawk',
+ 'hazard',
+ 'head',
+ 'health',
+ 'heart',
+ 'heavy',
+ 'hedgehog',
+ 'height',
+ 'hello',
+ 'helmet',
+ 'help',
+ 'hen',
+ 'hero',
+ 'hidden',
+ 'high',
+ 'hill',
+ 'hint',
+ 'hip',
+ 'hire',
+ 'history',
+ 'hobby',
+ 'hockey',
+ 'hold',
+ 'hole',
+ 'holiday',
+ 'hollow',
+ 'home',
+ 'honey',
+ 'hood',
+ 'hope',
+ 'horn',
+ 'horror',
+ 'horse',
+ 'hospital',
+ 'host',
+ 'hotel',
+ 'hour',
+ 'hover',
+ 'hub',
+ 'huge',
+ 'human',
+ 'humble',
+ 'humor',
+ 'hundred',
+ 'hungry',
+ 'hunt',
+ 'hurdle',
+ 'hurry',
+ 'hurt',
+ 'husband',
+ 'hybrid',
+ 'ice',
+ 'icon',
+ 'idea',
+ 'identify',
+ 'idle',
+ 'ignore',
+ 'ill',
+ 'illegal',
+ 'illness',
+ 'image',
+ 'imitate',
+ 'immense',
+ 'immune',
+ 'impact',
+ 'impose',
+ 'improve',
+ 'impulse',
+ 'inch',
+ 'include',
+ 'income',
+ 'increase',
+ 'index',
+ 'indicate',
+ 'indoor',
+ 'industry',
+ 'infant',
+ 'inflict',
+ 'inform',
+ 'inhale',
+ 'inherit',
+ 'initial',
+ 'inject',
+ 'injury',
+ 'inmate',
+ 'inner',
+ 'innocent',
+ 'input',
+ 'inquiry',
+ 'insane',
+ 'insect',
+ 'inside',
+ 'inspire',
+ 'install',
+ 'intact',
+ 'interest',
+ 'into',
+ 'invest',
+ 'invite',
+ 'involve',
+ 'iron',
+ 'island',
+ 'isolate',
+ 'issue',
+ 'item',
+ 'ivory',
+ 'jacket',
+ 'jaguar',
+ 'jar',
+ 'jazz',
+ 'jealous',
+ 'jeans',
+ 'jelly',
+ 'jewel',
+ 'job',
+ 'join',
+ 'joke',
+ 'journey',
+ 'joy',
+ 'judge',
+ 'juice',
+ 'jump',
+ 'jungle',
+ 'junior',
+ 'junk',
+ 'just',
+ 'kangaroo',
+ 'keen',
+ 'keep',
+ 'ketchup',
+ 'key',
+ 'kick',
+ 'kid',
+ 'kidney',
+ 'kind',
+ 'kingdom',
+ 'kiss',
+ 'kit',
+ 'kitchen',
+ 'kite',
+ 'kitten',
+ 'kiwi',
+ 'knee',
+ 'knife',
+ 'knock',
+ 'know',
+ 'lab',
+ 'label',
+ 'labor',
+ 'ladder',
+ 'lady',
+ 'lake',
+ 'lamp',
+ 'language',
+ 'laptop',
+ 'large',
+ 'later',
+ 'latin',
+ 'laugh',
+ 'laundry',
+ 'lava',
+ 'law',
+ 'lawn',
+ 'lawsuit',
+ 'layer',
+ 'lazy',
+ 'leader',
+ 'leaf',
+ 'learn',
+ 'leave',
+ 'lecture',
+ 'left',
+ 'leg',
+ 'legal',
+ 'legend',
+ 'leisure',
+ 'lemon',
+ 'lend',
+ 'length',
+ 'lens',
+ 'leopard',
+ 'lesson',
+ 'letter',
+ 'level',
+ 'liar',
+ 'liberty',
+ 'library',
+ 'license',
+ 'life',
+ 'lift',
+ 'light',
+ 'like',
+ 'limb',
+ 'limit',
+ 'link',
+ 'lion',
+ 'liquid',
+ 'list',
+ 'little',
+ 'live',
+ 'lizard',
+ 'load',
+ 'loan',
+ 'lobster',
+ 'local',
+ 'lock',
+ 'logic',
+ 'lonely',
+ 'long',
+ 'loop',
+ 'lottery',
+ 'loud',
+ 'lounge',
+ 'love',
+ 'loyal',
+ 'lucky',
+ 'luggage',
+ 'lumber',
+ 'lunar',
+ 'lunch',
+ 'luxury',
+ 'lyrics',
+ 'machine',
+ 'mad',
+ 'magic',
+ 'magnet',
+ 'maid',
+ 'mail',
+ 'main',
+ 'major',
+ 'make',
+ 'mammal',
+ 'man',
+ 'manage',
+ 'mandate',
+ 'mango',
+ 'mansion',
+ 'manual',
+ 'maple',
+ 'marble',
+ 'march',
+ 'margin',
+ 'marine',
+ 'market',
+ 'marriage',
+ 'mask',
+ 'mass',
+ 'master',
+ 'match',
+ 'material',
+ 'math',
+ 'matrix',
+ 'matter',
+ 'maximum',
+ 'maze',
+ 'meadow',
+ 'mean',
+ 'measure',
+ 'meat',
+ 'mechanic',
+ 'medal',
+ 'media',
+ 'melody',
+ 'melt',
+ 'member',
+ 'memory',
+ 'mention',
+ 'menu',
+ 'mercy',
+ 'merge',
+ 'merit',
+ 'merry',
+ 'mesh',
+ 'message',
+ 'metal',
+ 'method',
+ 'middle',
+ 'midnight',
+ 'milk',
+ 'million',
+ 'mimic',
+ 'mind',
+ 'minimum',
+ 'minor',
+ 'minute',
+ 'miracle',
+ 'mirror',
+ 'misery',
+ 'miss',
+ 'mistake',
+ 'mix',
+ 'mixed',
+ 'mixture',
+ 'mobile',
+ 'model',
+ 'modify',
+ 'mom',
+ 'moment',
+ 'monitor',
+ 'monkey',
+ 'monster',
+ 'month',
+ 'moon',
+ 'moral',
+ 'more',
+ 'morning',
+ 'mosquito',
+ 'mother',
+ 'motion',
+ 'motor',
+ 'mountain',
+ 'mouse',
+ 'move',
+ 'movie',
+ 'much',
+ 'muffin',
+ 'mule',
+ 'multiply',
+ 'muscle',
+ 'museum',
+ 'mushroom',
+ 'music',
+ 'must',
+ 'mutual',
+ 'myself',
+ 'mystery',
+ 'myth',
+ 'naive',
+ 'name',
+ 'napkin',
+ 'narrow',
+ 'nasty',
+ 'nation',
+ 'nature',
+ 'near',
+ 'neck',
+ 'need',
+ 'negative',
+ 'neglect',
+ 'neither',
+ 'nephew',
+ 'nerve',
+ 'nest',
+ 'net',
+ 'network',
+ 'neutral',
+ 'never',
+ 'news',
+ 'next',
+ 'nice',
+ 'night',
+ 'noble',
+ 'noise',
+ 'nominee',
+ 'noodle',
+ 'normal',
+ 'north',
+ 'nose',
+ 'notable',
+ 'note',
+ 'nothing',
+ 'notice',
+ 'novel',
+ 'now',
+ 'nuclear',
+ 'number',
+ 'nurse',
+ 'nut',
+ 'oak',
+ 'obey',
+ 'object',
+ 'oblige',
+ 'obscure',
+ 'observe',
+ 'obtain',
+ 'obvious',
+ 'occur',
+ 'ocean',
+ 'october',
+ 'odor',
+ 'off',
+ 'offer',
+ 'office',
+ 'often',
+ 'oil',
+ 'okay',
+ 'old',
+ 'olive',
+ 'olympic',
+ 'omit',
+ 'once',
+ 'one',
+ 'onion',
+ 'online',
+ 'only',
+ 'open',
+ 'opera',
+ 'opinion',
+ 'oppose',
+ 'option',
+ 'orange',
+ 'orbit',
+ 'orchard',
+ 'order',
+ 'ordinary',
+ 'organ',
+ 'orient',
+ 'original',
+ 'orphan',
+ 'ostrich',
+ 'other',
+ 'outdoor',
+ 'outer',
+ 'output',
+ 'outside',
+ 'oval',
+ 'oven',
+ 'over',
+ 'own',
+ 'owner',
+ 'oxygen',
+ 'oyster',
+ 'ozone',
+ 'pact',
+ 'paddle',
+ 'page',
+ 'pair',
+ 'palace',
+ 'palm',
+ 'panda',
+ 'panel',
+ 'panic',
+ 'panther',
+ 'paper',
+ 'parade',
+ 'parent',
+ 'park',
+ 'parrot',
+ 'party',
+ 'pass',
+ 'patch',
+ 'path',
+ 'patient',
+ 'patrol',
+ 'pattern',
+ 'pause',
+ 'pave',
+ 'payment',
+ 'peace',
+ 'peanut',
+ 'pear',
+ 'peasant',
+ 'pelican',
+ 'pen',
+ 'penalty',
+ 'pencil',
+ 'people',
+ 'pepper',
+ 'perfect',
+ 'permit',
+ 'person',
+ 'pet',
+ 'phone',
+ 'photo',
+ 'phrase',
+ 'physical',
+ 'piano',
+ 'picnic',
+ 'picture',
+ 'piece',
+ 'pig',
+ 'pigeon',
+ 'pill',
+ 'pilot',
+ 'pink',
+ 'pioneer',
+ 'pipe',
+ 'pistol',
+ 'pitch',
+ 'pizza',
+ 'place',
+ 'planet',
+ 'plastic',
+ 'plate',
+ 'play',
+ 'please',
+ 'pledge',
+ 'pluck',
+ 'plug',
+ 'plunge',
+ 'poem',
+ 'poet',
+ 'point',
+ 'polar',
+ 'pole',
+ 'police',
+ 'pond',
+ 'pony',
+ 'pool',
+ 'popular',
+ 'portion',
+ 'position',
+ 'possible',
+ 'post',
+ 'potato',
+ 'pottery',
+ 'poverty',
+ 'powder',
+ 'power',
+ 'practice',
+ 'praise',
+ 'predict',
+ 'prefer',
+ 'prepare',
+ 'present',
+ 'pretty',
+ 'prevent',
+ 'price',
+ 'pride',
+ 'primary',
+ 'print',
+ 'priority',
+ 'prison',
+ 'private',
+ 'prize',
+ 'problem',
+ 'process',
+ 'produce',
+ 'profit',
+ 'program',
+ 'project',
+ 'promote',
+ 'proof',
+ 'property',
+ 'prosper',
+ 'protect',
+ 'proud',
+ 'provide',
+ 'public',
+ 'pudding',
+ 'pull',
+ 'pulp',
+ 'pulse',
+ 'pumpkin',
+ 'punch',
+ 'pupil',
+ 'puppy',
+ 'purchase',
+ 'purity',
+ 'purpose',
+ 'purse',
+ 'push',
+ 'put',
+ 'puzzle',
+ 'pyramid',
+ 'quality',
+ 'quantum',
+ 'quarter',
+ 'question',
+ 'quick',
+ 'quit',
+ 'quiz',
+ 'quote',
+ 'rabbit',
+ 'raccoon',
+ 'race',
+ 'rack',
+ 'radar',
+ 'radio',
+ 'rail',
+ 'rain',
+ 'raise',
+ 'rally',
+ 'ramp',
+ 'ranch',
+ 'random',
+ 'range',
+ 'rapid',
+ 'rare',
+ 'rate',
+ 'rather',
+ 'raven',
+ 'raw',
+ 'razor',
+ 'ready',
+ 'real',
+ 'reason',
+ 'rebel',
+ 'rebuild',
+ 'recall',
+ 'receive',
+ 'recipe',
+ 'record',
+ 'recycle',
+ 'reduce',
+ 'reflect',
+ 'reform',
+ 'refuse',
+ 'region',
+ 'regret',
+ 'regular',
+ 'reject',
+ 'relax',
+ 'release',
+ 'relief',
+ 'rely',
+ 'remain',
+ 'remember',
+ 'remind',
+ 'remove',
+ 'render',
+ 'renew',
+ 'rent',
+ 'reopen',
+ 'repair',
+ 'repeat',
+ 'replace',
+ 'report',
+ 'require',
+ 'rescue',
+ 'resemble',
+ 'resist',
+ 'resource',
+ 'response',
+ 'result',
+ 'retire',
+ 'retreat',
+ 'return',
+ 'reunion',
+ 'reveal',
+ 'review',
+ 'reward',
+ 'rhythm',
+ 'rib',
+ 'ribbon',
+ 'rice',
+ 'rich',
+ 'ride',
+ 'ridge',
+ 'rifle',
+ 'right',
+ 'rigid',
+ 'ring',
+ 'riot',
+ 'ripple',
+ 'risk',
+ 'ritual',
+ 'rival',
+ 'river',
+ 'road',
+ 'roast',
+ 'robot',
+ 'robust',
+ 'rocket',
+ 'romance',
+ 'roof',
+ 'rookie',
+ 'room',
+ 'rose',
+ 'rotate',
+ 'rough',
+ 'round',
+ 'route',
+ 'royal',
+ 'rubber',
+ 'rude',
+ 'rug',
+ 'rule',
+ 'run',
+ 'runway',
+ 'rural',
+ 'sad',
+ 'saddle',
+ 'sadness',
+ 'safe',
+ 'sail',
+ 'salad',
+ 'salmon',
+ 'salon',
+ 'salt',
+ 'salute',
+ 'same',
+ 'sample',
+ 'sand',
+ 'satisfy',
+ 'satoshi',
+ 'sauce',
+ 'sausage',
+ 'save',
+ 'say',
+ 'scale',
+ 'scan',
+ 'scare',
+ 'scatter',
+ 'scene',
+ 'scheme',
+ 'school',
+ 'science',
+ 'scissors',
+ 'scorpion',
+ 'scout',
+ 'scrap',
+ 'screen',
+ 'script',
+ 'scrub',
+ 'sea',
+ 'search',
+ 'season',
+ 'seat',
+ 'second',
+ 'secret',
+ 'section',
+ 'security',
+ 'seed',
+ 'seek',
+ 'segment',
+ 'select',
+ 'sell',
+ 'seminar',
+ 'senior',
+ 'sense',
+ 'sentence',
+ 'series',
+ 'service',
+ 'session',
+ 'settle',
+ 'setup',
+ 'seven',
+ 'shadow',
+ 'shaft',
+ 'shallow',
+ 'share',
+ 'shed',
+ 'shell',
+ 'sheriff',
+ 'shield',
+ 'shift',
+ 'shine',
+ 'ship',
+ 'shiver',
+ 'shock',
+ 'shoe',
+ 'shoot',
+ 'shop',
+ 'short',
+ 'shoulder',
+ 'shove',
+ 'shrimp',
+ 'shrug',
+ 'shuffle',
+ 'shy',
+ 'sibling',
+ 'sick',
+ 'side',
+ 'siege',
+ 'sight',
+ 'sign',
+ 'silent',
+ 'silk',
+ 'silly',
+ 'silver',
+ 'similar',
+ 'simple',
+ 'since',
+ 'sing',
+ 'siren',
+ 'sister',
+ 'situate',
+ 'six',
+ 'size',
+ 'skate',
+ 'sketch',
+ 'ski',
+ 'skill',
+ 'skin',
+ 'skirt',
+ 'skull',
+ 'slab',
+ 'slam',
+ 'sleep',
+ 'slender',
+ 'slice',
+ 'slide',
+ 'slight',
+ 'slim',
+ 'slogan',
+ 'slot',
+ 'slow',
+ 'slush',
+ 'small',
+ 'smart',
+ 'smile',
+ 'smoke',
+ 'smooth',
+ 'snack',
+ 'snake',
+ 'snap',
+ 'sniff',
+ 'snow',
+ 'soap',
+ 'soccer',
+ 'social',
+ 'sock',
+ 'soda',
+ 'soft',
+ 'solar',
+ 'soldier',
+ 'solid',
+ 'solution',
+ 'solve',
+ 'someone',
+ 'song',
+ 'soon',
+ 'sorry',
+ 'sort',
+ 'soul',
+ 'sound',
+ 'soup',
+ 'source',
+ 'south',
+ 'space',
+ 'spare',
+ 'spatial',
+ 'spawn',
+ 'speak',
+ 'special',
+ 'speed',
+ 'spell',
+ 'spend',
+ 'sphere',
+ 'spice',
+ 'spider',
+ 'spike',
+ 'spin',
+ 'spirit',
+ 'split',
+ 'spoil',
+ 'sponsor',
+ 'spoon',
+ 'sport',
+ 'spot',
+ 'spray',
+ 'spread',
+ 'spring',
+ 'spy',
+ 'square',
+ 'squeeze',
+ 'squirrel',
+ 'stable',
+ 'stadium',
+ 'staff',
+ 'stage',
+ 'stairs',
+ 'stamp',
+ 'stand',
+ 'start',
+ 'state',
+ 'stay',
+ 'steak',
+ 'steel',
+ 'stem',
+ 'step',
+ 'stereo',
+ 'stick',
+ 'still',
+ 'sting',
+ 'stock',
+ 'stomach',
+ 'stone',
+ 'stool',
+ 'story',
+ 'stove',
+ 'strategy',
+ 'street',
+ 'strike',
+ 'strong',
+ 'struggle',
+ 'student',
+ 'stuff',
+ 'stumble',
+ 'style',
+ 'subject',
+ 'submit',
+ 'subway',
+ 'success',
+ 'such',
+ 'sudden',
+ 'suffer',
+ 'sugar',
+ 'suggest',
+ 'suit',
+ 'summer',
+ 'sun',
+ 'sunny',
+ 'sunset',
+ 'super',
+ 'supply',
+ 'supreme',
+ 'sure',
+ 'surface',
+ 'surge',
+ 'surprise',
+ 'surround',
+ 'survey',
+ 'suspect',
+ 'sustain',
+ 'swallow',
+ 'swamp',
+ 'swap',
+ 'swarm',
+ 'swear',
+ 'sweet',
+ 'swift',
+ 'swim',
+ 'swing',
+ 'switch',
+ 'sword',
+ 'symbol',
+ 'symptom',
+ 'syrup',
+ 'system',
+ 'table',
+ 'tackle',
+ 'tag',
+ 'tail',
+ 'talent',
+ 'talk',
+ 'tank',
+ 'tape',
+ 'target',
+ 'task',
+ 'taste',
+ 'tattoo',
+ 'taxi',
+ 'teach',
+ 'team',
+ 'tell',
+ 'ten',
+ 'tenant',
+ 'tennis',
+ 'tent',
+ 'term',
+ 'test',
+ 'text',
+ 'thank',
+ 'that',
+ 'theme',
+ 'then',
+ 'theory',
+ 'there',
+ 'they',
+ 'thing',
+ 'this',
+ 'thought',
+ 'three',
+ 'thrive',
+ 'throw',
+ 'thumb',
+ 'thunder',
+ 'ticket',
+ 'tide',
+ 'tiger',
+ 'tilt',
+ 'timber',
+ 'time',
+ 'tiny',
+ 'tip',
+ 'tired',
+ 'tissue',
+ 'title',
+ 'toast',
+ 'tobacco',
+ 'today',
+ 'toddler',
+ 'toe',
+ 'together',
+ 'toilet',
+ 'token',
+ 'tomato',
+ 'tomorrow',
+ 'tone',
+ 'tongue',
+ 'tonight',
+ 'tool',
+ 'tooth',
+ 'top',
+ 'topic',
+ 'topple',
+ 'torch',
+ 'tornado',
+ 'tortoise',
+ 'toss',
+ 'total',
+ 'tourist',
+ 'toward',
+ 'tower',
+ 'town',
+ 'toy',
+ 'track',
+ 'trade',
+ 'traffic',
+ 'tragic',
+ 'train',
+ 'transfer',
+ 'trap',
+ 'trash',
+ 'travel',
+ 'tray',
+ 'treat',
+ 'tree',
+ 'trend',
+ 'trial',
+ 'tribe',
+ 'trick',
+ 'trigger',
+ 'trim',
+ 'trip',
+ 'trophy',
+ 'trouble',
+ 'truck',
+ 'true',
+ 'truly',
+ 'trumpet',
+ 'trust',
+ 'truth',
+ 'try',
+ 'tube',
+ 'tuition',
+ 'tumble',
+ 'tuna',
+ 'tunnel',
+ 'turkey',
+ 'turn',
+ 'turtle',
+ 'twelve',
+ 'twenty',
+ 'twice',
+ 'twin',
+ 'twist',
+ 'two',
+ 'type',
+ 'typical',
+ 'ugly',
+ 'umbrella',
+ 'unable',
+ 'unaware',
+ 'uncle',
+ 'uncover',
+ 'under',
+ 'undo',
+ 'unfair',
+ 'unfold',
+ 'unhappy',
+ 'uniform',
+ 'unique',
+ 'unit',
+ 'universe',
+ 'unknown',
+ 'unlock',
+ 'until',
+ 'unusual',
+ 'unveil',
+ 'update',
+ 'upgrade',
+ 'uphold',
+ 'upon',
+ 'upper',
+ 'upset',
+ 'urban',
+ 'urge',
+ 'usage',
+ 'use',
+ 'used',
+ 'useful',
+ 'useless',
+ 'usual',
+ 'utility',
+ 'vacant',
+ 'vacuum',
+ 'vague',
+ 'valid',
+ 'valley',
+ 'valve',
+ 'van',
+ 'vanish',
+ 'vapor',
+ 'various',
+ 'vast',
+ 'vault',
+ 'vehicle',
+ 'velvet',
+ 'vendor',
+ 'venture',
+ 'venue',
+ 'verb',
+ 'verify',
+ 'version',
+ 'very',
+ 'vessel',
+ 'veteran',
+ 'viable',
+ 'vibrant',
+ 'vicious',
+ 'victory',
+ 'video',
+ 'view',
+ 'village',
+ 'vintage',
+ 'violin',
+ 'virtual',
+ 'virus',
+ 'visa',
+ 'visit',
+ 'visual',
+ 'vital',
+ 'vivid',
+ 'vocal',
+ 'voice',
+ 'void',
+ 'volcano',
+ 'volume',
+ 'vote',
+ 'voyage',
+ 'wage',
+ 'wagon',
+ 'wait',
+ 'walk',
+ 'wall',
+ 'walnut',
+ 'want',
+ 'warfare',
+ 'warm',
+ 'warrior',
+ 'wash',
+ 'wasp',
+ 'waste',
+ 'water',
+ 'wave',
+ 'way',
+ 'wealth',
+ 'weapon',
+ 'wear',
+ 'weasel',
+ 'weather',
+ 'web',
+ 'wedding',
+ 'weekend',
+ 'weird',
+ 'welcome',
+ 'west',
+ 'wet',
+ 'whale',
+ 'what',
+ 'wheat',
+ 'wheel',
+ 'when',
+ 'where',
+ 'whip',
+ 'whisper',
+ 'wide',
+ 'width',
+ 'wife',
+ 'wild',
+ 'will',
+ 'win',
+ 'window',
+ 'wine',
+ 'wing',
+ 'wink',
+ 'winner',
+ 'winter',
+ 'wire',
+ 'wisdom',
+ 'wise',
+ 'wish',
+ 'witness',
+ 'wolf',
+ 'woman',
+ 'wonder',
+ 'wood',
+ 'wool',
+ 'word',
+ 'work',
+ 'world',
+ 'worry',
+ 'worth',
+ 'wrap',
+ 'wreck',
+ 'wrestle',
+ 'wrist',
+ 'write',
+ 'wrong',
+ 'yard',
+ 'year',
+ 'yellow',
+ 'you',
+ 'young',
+ 'youth',
+ 'zebra',
+ 'zero',
+ 'zone',
+ 'zoo'
+ ];
+}
diff --git a/cw_solana/lib/solana_transaction_credentials.dart b/cw_solana/lib/solana_transaction_credentials.dart
new file mode 100644
index 000000000..bd0c97f0b
--- /dev/null
+++ b/cw_solana/lib/solana_transaction_credentials.dart
@@ -0,0 +1,12 @@
+import 'package:cw_core/crypto_currency.dart';
+import 'package:cw_core/output_info.dart';
+
+class SolanaTransactionCredentials {
+ SolanaTransactionCredentials(
+ this.outputs, {
+ required this.currency,
+ });
+
+ final List outputs;
+ final CryptoCurrency currency;
+}
diff --git a/cw_solana/lib/solana_transaction_history.dart b/cw_solana/lib/solana_transaction_history.dart
new file mode 100644
index 000000000..c03de19ad
--- /dev/null
+++ b/cw_solana/lib/solana_transaction_history.dart
@@ -0,0 +1,78 @@
+import 'dart:convert';
+import 'dart:core';
+import 'package:cw_core/pathForWallet.dart';
+import 'package:cw_core/wallet_info.dart';
+import 'package:cw_solana/file.dart';
+import 'package:cw_solana/solana_transaction_info.dart';
+import 'package:mobx/mobx.dart';
+import 'package:cw_core/transaction_history.dart';
+
+part 'solana_transaction_history.g.dart';
+
+const transactionsHistoryFileName = 'solana_transactions.json';
+
+class SolanaTransactionHistory = SolanaTransactionHistoryBase with _$SolanaTransactionHistory;
+
+abstract class SolanaTransactionHistoryBase extends TransactionHistoryBase
+ with Store {
+ SolanaTransactionHistoryBase({required this.walletInfo, required String password})
+ : _password = password {
+ transactions = ObservableMap();
+ }
+
+ final WalletInfo walletInfo;
+ String _password;
+
+ Future init() async => await _load();
+
+ @override
+ Future save() async {
+ try {
+ final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
+ final path = '$dirPath/$transactionsHistoryFileName';
+ final transactionMaps = transactions.map((key, value) => MapEntry(key, value.toJson()));
+ final data = json.encode({'transactions': transactionMaps});
+ await writeData(path: path, password: _password, data: data);
+ } catch (e, s) {
+ print('Error while saving solana transaction history: ${e.toString()}');
+ print(s);
+ }
+ }
+
+ @override
+ void addOne(SolanaTransactionInfo transaction) => transactions[transaction.id] = transaction;
+
+ @override
+ void addMany(Map transactions) =>
+ this.transactions.addAll(transactions);
+
+ Future