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>
This commit is contained in:
Adegoke David 2024-02-23 14:39:19 +01:00 committed by GitHub
parent 5a7ea87543
commit 109bba4301
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
133 changed files with 5356 additions and 353 deletions

View file

@ -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:
@ -111,6 +111,7 @@ jobs:
cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
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_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
@ -120,6 +121,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
@ -154,6 +156,7 @@ jobs:
echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart
echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/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
@ -163,18 +166,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: |

3
.gitignore vendored
View file

@ -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/
@ -128,6 +130,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

View file

@ -66,6 +66,7 @@
<data android:scheme="polygon" />
<data android:scheme="polygon-wallet" />
<data android:scheme="polygon_wallet" />
<data android:scheme="solana-wallet" />
</intent-filter>
</activity>
<meta-data

BIN
assets/images/avdo_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
assets/images/bonk_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
assets/images/gmt_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/images/hnt_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
assets/images/ray_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,4 @@
-
uri: rpc.ankr.com
is_default: true
useSSL: true

View file

@ -9,7 +9,8 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> 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<int> with Serializable<int> 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<int> with Serializable<int> 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<int, CryptoCurrency> _rawCurrencyMap =

View file

@ -21,6 +21,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');
}

View file

@ -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;
const DERIVATION_TYPE_TYPE_ID = 15;
const SPL_TOKEN_TYPE_ID = 16;

View file

@ -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;

View file

@ -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<String> pathForWalletDir({required String name, required WalletType type}) async {

View file

@ -14,6 +14,7 @@ const walletTypes = [
WalletType.nano,
WalletType.banano,
WalletType.polygon,
WalletType.solana,
];
@HiveType(typeId: WALLET_TYPE_TYPE_ID)
@ -46,7 +47,10 @@ enum WalletType {
bitcoinCash,
@HiveField(9)
polygon
polygon,
@HiveField(10)
solana
}
int serializeToInt(WalletType type) {
@ -69,6 +73,8 @@ int serializeToInt(WalletType type) {
return 7;
case WalletType.polygon:
return 8;
case WalletType.solana:
return 9;
default:
return -1;
}
@ -94,6 +100,8 @@ WalletType deserializeFromInt(int raw) {
return WalletType.bitcoinCash;
case 8:
return WalletType.polygon;
case 9:
return WalletType.solana;
default:
throw Exception('Unexpected token: $raw for WalletType deserializeFromInt');
}
@ -119,6 +127,8 @@ String walletTypeToString(WalletType type) {
return 'Banano';
case WalletType.polygon:
return 'Polygon';
case WalletType.solana:
return 'Solana';
default:
return '';
}
@ -144,6 +154,8 @@ String walletTypeToDisplayName(WalletType type) {
return 'Banano (BAN)';
case WalletType.polygon:
return 'Polygon (MATIC)';
case WalletType.solana:
return 'Solana (SOL)';
default:
return '';
}
@ -169,6 +181,8 @@ CryptoCurrency walletTypeToCryptoCurrency(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 walletTypeToCryptoCurrency');

30
cw_solana/.gitignore vendored Normal file
View file

@ -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/

10
cw_solana/.metadata Normal file
View file

@ -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

3
cw_solana/CHANGELOG.md Normal file
View file

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

1
cw_solana/LICENSE Normal file
View file

@ -0,0 +1 @@
TODO: Add your license here.

39
cw_solana/README.md Normal file
View file

@ -0,0 +1,39 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/developing-packages).
-->
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.

View file

@ -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

View file

@ -0,0 +1,7 @@
library cw_solana;
/// A Calculator.
class Calculator {
/// Returns [value] plus 1.
int addOne(int value) => value + 1;
}

View file

@ -0,0 +1,109 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_solana/spl_token.dart';
class DefaultSPLTokens {
final List<SPLToken> _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<SPLToken> 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();
}

39
cw_solana/lib/file.dart Normal file
View file

@ -0,0 +1,39 @@
import 'dart:io';
import 'package:cw_core/key.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
Future<void> 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<void> 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<String> 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);
}

View file

@ -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<void> commit() async {
return await sendTransaction();
}
@override
String get feeFormatted => fee.toString();
@override
String get hex => signedTransaction.encode();
@override
String get id => '';
}

View file

@ -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()});
}

View file

@ -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<double> getBalance(String address) async {
try {
final balance = await _client!.rpcClient.getBalance(address);
final solBalance = balance.value / lamportsPerSol;
return solBalance;
} catch (_) {
return 0.0;
}
}
Future<ProgramAccountsResult?> 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<SolanaBalance?> 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<double> 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<List<SolanaTransactionModel>> fetchTransactions(
Ed25519HDPublicKey publicKey, {
String? splTokenSymbol,
int? splTokenDecimal,
}) async {
List<SolanaTransactionModel> 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<List<SolanaTransactionModel>> 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<PendingSolanaTransaction> signSolanaTransaction({
required String tokenTitle,
required int tokenDecimals,
String? tokenMint,
required double inputAmount,
required String destinationAddress,
required Ed25519HDKeyPair ownerKeypair,
List<String> 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<PendingSolanaTransaction> _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<PendingSolanaTransaction> _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<double> _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<SignedTx> _signTransactionInternal({
required Message message,
required List<Ed25519HDKeyPair> signers,
required Commitment commitment,
required RecentBlockhash recentBlockhash,
}) async {
final signedTx = await signTransaction(recentBlockhash, message, signers);
return signedTx;
}
Future<String> sendTransaction({
required SignedTx signedTransaction,
required Commitment commitment,
}) async {
final signature = await _client!.rpcClient.sendTransaction(signedTransaction.encode());
_client!.waitForSignatureStatus(signature, status: commitment);
return signature;
}
}

View file

@ -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;
}

File diff suppressed because it is too large Load diff

View file

@ -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<OutputInfo> outputs;
final CryptoCurrency currency;
}

View file

@ -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<SolanaTransactionInfo>
with Store {
SolanaTransactionHistoryBase({required this.walletInfo, required String password})
: _password = password {
transactions = ObservableMap<String, SolanaTransactionInfo>();
}
final WalletInfo walletInfo;
String _password;
Future<void> init() async => await _load();
@override
Future<void> 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<String, SolanaTransactionInfo> transactions) =>
this.transactions.addAll(transactions);
Future<Map<String, dynamic>> _read() async {
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
final path = '$dirPath/$transactionsHistoryFileName';
final content = await read(path: path, password: _password);
if (content.isEmpty) {
return {};
}
return json.decode(content) as Map<String, dynamic>;
}
Future<void> _load() async {
try {
final content = await _read();
final txs = content['transactions'] as Map<String, dynamic>? ?? {};
txs.entries.forEach((entry) {
final val = entry.value;
if (val is Map<String, dynamic>) {
final tx = SolanaTransactionInfo.fromJson(val);
_update(tx);
}
});
} catch (e) {
print(e);
}
}
void _update(SolanaTransactionInfo transaction) => transactions[transaction.id] = transaction;
}

View file

@ -0,0 +1,78 @@
import 'package:cw_core/format_amount.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart';
class SolanaTransactionInfo extends TransactionInfo {
SolanaTransactionInfo({
required this.id,
required this.blockTime,
required this.to,
required this.from,
required this.direction,
required this.solAmount,
this.tokenSymbol = "SOL",
required this.isPending,
required this.txFee,
}) : amount = solAmount.toInt();
final String id;
final String? to;
final String? from;
final int amount;
final bool isPending;
final double solAmount;
final String tokenSymbol;
final DateTime blockTime;
final double txFee;
final TransactionDirection direction;
String? _fiatAmount;
@override
DateTime get date => blockTime;
@override
String amountFormatted() {
String stringBalance = solAmount.toString();
if (stringBalance.toString().length >= 6) {
stringBalance = stringBalance.substring(0, 6);
}
return '$stringBalance $tokenSymbol';
}
@override
String fiatAmount() => _fiatAmount ?? '';
@override
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
@override
String feeFormatted() => '${txFee.toString()} SOL';
factory SolanaTransactionInfo.fromJson(Map<String, dynamic> data) {
return SolanaTransactionInfo(
id: data['id'] as String,
solAmount: data['solAmount'],
direction: parseTransactionDirectionFromInt(data['direction'] as int),
blockTime: DateTime.fromMillisecondsSinceEpoch(data['blockTime'] as int),
isPending: data['isPending'] as bool,
tokenSymbol: data['tokenSymbol'] as String,
to: data['to'],
from: data['from'],
txFee: data['txFee'],
);
}
Map<String, dynamic> toJson() => {
'id': id,
'solAmount': solAmount,
'direction': direction.index,
'blockTime': blockTime.millisecondsSinceEpoch,
'isPending': isPending,
'tokenSymbol': tokenSymbol,
'to': to,
'from': from,
'txFee': txFee,
};
}

View file

@ -0,0 +1,47 @@
class SolanaTransactionModel {
final String id;
final String from;
final String to;
final double amount;
// If this is an outgoing transaction
final bool isOutgoingTx;
// The Program ID of this transaction, e.g, System Program, Token Program...
final String programId;
// The DateTime from the UNIX timestamp of the block where the transaction was included
final DateTime blockTime;
// The Transaction fee
final double fee;
// The token symbol
final String tokenSymbol;
SolanaTransactionModel({
required this.id,
required this.to,
required this.from,
required this.amount,
required this.programId,
required int blockTimeInInt,
this.isOutgoingTx = false,
required this.tokenSymbol,
required this.fee,
}) : blockTime = DateTime.fromMillisecondsSinceEpoch(blockTimeInInt * 1000);
factory SolanaTransactionModel.fromJson(Map<String, dynamic> json) => SolanaTransactionModel(
id: json['id'],
blockTimeInInt: int.parse(json["timeStamp"]) * 1000,
from: json["from"],
to: json["to"],
amount: double.parse(json["value"]),
programId: json["programId"],
fee: json['fee'],
tokenSymbol: json['tokenSymbol'],
);
}

View file

@ -0,0 +1,510 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_solana/default_spl_tokens.dart';
import 'package:cw_solana/file.dart';
import 'package:cw_solana/solana_balance.dart';
import 'package:cw_solana/solana_client.dart';
import 'package:cw_solana/solana_exceptions.dart';
import 'package:cw_solana/solana_transaction_credentials.dart';
import 'package:cw_solana/solana_transaction_history.dart';
import 'package:cw_solana/solana_transaction_info.dart';
import 'package:cw_solana/solana_transaction_model.dart';
import 'package:cw_solana/solana_wallet_addresses.dart';
import 'package:cw_solana/spl_token.dart';
import 'package:hex/hex.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solana/metaplex.dart' as metaplex;
import 'package:solana/solana.dart';
import 'package:web3dart/crypto.dart';
part 'solana_wallet.g.dart';
class SolanaWallet = SolanaWalletBase with _$SolanaWallet;
abstract class SolanaWalletBase
extends WalletBase<SolanaBalance, SolanaTransactionHistory, SolanaTransactionInfo> with Store {
SolanaWalletBase({
required WalletInfo walletInfo,
String? mnemonic,
String? privateKey,
required String password,
SolanaBalance? initialBalance,
}) : syncStatus = const NotConnectedSyncStatus(),
_password = password,
_mnemonic = mnemonic,
_hexPrivateKey = privateKey,
_client = SolanaWalletClient(),
walletAddresses = SolanaWalletAddresses(walletInfo),
balance = ObservableMap<CryptoCurrency, SolanaBalance>.of(
{CryptoCurrency.sol: initialBalance ?? SolanaBalance(BigInt.zero.toDouble())}),
super(walletInfo) {
this.walletInfo = walletInfo;
transactionHistory = SolanaTransactionHistory(walletInfo: walletInfo, password: password);
if (!CakeHive.isAdapterRegistered(SPLToken.typeId)) {
CakeHive.registerAdapter(SPLTokenAdapter());
}
_sharedPrefs.complete(SharedPreferences.getInstance());
}
final String _password;
final String? _mnemonic;
final String? _hexPrivateKey;
// The Solana WalletPair
Ed25519HDKeyPair? _walletKeyPair;
Ed25519HDKeyPair? get walletKeyPair => _walletKeyPair;
// To access the privateKey bytes.
Ed25519HDKeyPairData? _keyPairData;
late SolanaWalletClient _client;
Timer? _transactionsUpdateTimer;
late final Box<SPLToken> splTokensBox;
@override
WalletAddresses walletAddresses;
@override
@observable
SyncStatus syncStatus;
@override
@observable
late ObservableMap<CryptoCurrency, SolanaBalance> balance;
Completer<SharedPreferences> _sharedPrefs = Completer();
@override
Ed25519HDKeyPairData get keys {
if (_keyPairData == null) {
return Ed25519HDKeyPairData([], publicKey: const Ed25519HDPublicKey([]));
}
return _keyPairData!;
}
@override
String? get seed => _mnemonic;
@override
String get privateKey => HEX.encode(_keyPairData!.bytes);
Future<void> init() async {
final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${SPLToken.boxName}";
splTokensBox = await CakeHive.openBox<SPLToken>(boxName);
// Create WalletPair using either the mnemonic or the privateKey
_walletKeyPair = await getWalletPair(
mnemonic: _mnemonic,
privateKey: _hexPrivateKey,
);
// Extract the keyPairData containing both the privateKey bytes and the publicKey hex.
_keyPairData = await _walletKeyPair!.extract();
walletInfo.address = _walletKeyPair!.address;
await walletAddresses.init();
await transactionHistory.init();
await save();
}
Future<Wallet> getWalletPair({String? mnemonic, String? privateKey}) async {
assert(mnemonic != null || privateKey != null);
if (privateKey != null) {
final privateKeyBytes = hexToBytes(privateKey);
return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes);
}
return Wallet.fromMnemonic(mnemonic!, account: 0, change: 0);
}
@override
int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0;
@override
Future<void> changePassword(String password) => throw UnimplementedError("changePassword");
@override
void close() {
_client.stop();
_transactionsUpdateTimer?.cancel();
}
@action
@override
Future<void> connectToNode({required Node node}) async {
try {
syncStatus = ConnectingSyncStatus();
final isConnected = _client.connect(node);
if (!isConnected) {
throw Exception("Solana Node connection failed");
}
try {
await Future.wait([
_updateBalance(),
_updateNativeSOLTransactions(),
_updateSPLTokenTransactions(),
]);
} catch (e) {
log(e.toString());
}
_setTransactionUpdateTimer();
syncStatus = ConnectedSyncStatus();
} catch (e) {
syncStatus = FailedSyncStatus();
}
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
final solCredentials = credentials as SolanaTransactionCredentials;
final outputs = solCredentials.outputs;
final hasMultiDestination = outputs.length > 1;
await _updateBalance();
final CryptoCurrency transactionCurrency =
balance.keys.firstWhere((element) => element.title == solCredentials.currency.title);
final walletBalanceForCurrency = balance[transactionCurrency]!.balance;
double totalAmount = 0.0;
if (hasMultiDestination) {
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
throw SolanaTransactionWrongBalanceException(transactionCurrency);
}
final totalAmountFromCredentials =
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0));
totalAmount = totalAmountFromCredentials.toDouble();
if (walletBalanceForCurrency < totalAmount) {
throw SolanaTransactionWrongBalanceException(transactionCurrency);
}
} else {
final output = outputs.first;
final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0');
totalAmount = output.sendAll ? walletBalanceForCurrency : totalOriginalAmount;
if (walletBalanceForCurrency < totalAmount) {
throw SolanaTransactionWrongBalanceException(transactionCurrency);
}
}
String? tokenMint;
// Token Mint is only needed for transactions that are not native tokens(non-SOL transactions)
if (transactionCurrency.title != CryptoCurrency.sol.title) {
tokenMint = (transactionCurrency as SPLToken).mintAddress;
}
final pendingSolanaTransaction = await _client.signSolanaTransaction(
tokenMint: tokenMint,
tokenTitle: transactionCurrency.title,
inputAmount: totalAmount,
ownerKeypair: _walletKeyPair!,
tokenDecimals: transactionCurrency.decimals,
destinationAddress: solCredentials.outputs.first.isParsedAddress
? solCredentials.outputs.first.extractedAddress!
: solCredentials.outputs.first.address,
);
return pendingSolanaTransaction;
}
@override
Future<Map<String, SolanaTransactionInfo>> fetchTransactions() async => {};
/// Fetches the native SOL transactions linked to the wallet Public Key
Future<void> _updateNativeSOLTransactions() async {
final address = Ed25519HDPublicKey.fromBase58(_walletKeyPair!.address);
final transactions = await _client.fetchTransactions(address);
final Map<String, SolanaTransactionInfo> result = {};
for (var transactionModel in transactions) {
result[transactionModel.id] = SolanaTransactionInfo(
id: transactionModel.id,
to: transactionModel.to,
from: transactionModel.from,
blockTime: transactionModel.blockTime,
direction: transactionModel.isOutgoingTx
? TransactionDirection.outgoing
: TransactionDirection.incoming,
solAmount: transactionModel.amount,
isPending: false,
txFee: transactionModel.fee,
tokenSymbol: transactionModel.tokenSymbol,
);
}
transactionHistory.addMany(result);
await transactionHistory.save();
}
/// Fetches the SPL Tokens transactions linked to the token account Public Key
Future<void> _updateSPLTokenTransactions() async {
List<SolanaTransactionModel> splTokenTransactions = [];
for (var token in balance.keys) {
if (token is SPLToken) {
final tokenTxs = await _client.getSPLTokenTransfers(
token.mintAddress,
token.symbol,
token.decimal,
_walletKeyPair!,
);
splTokenTransactions.addAll(tokenTxs);
}
}
final Map<String, SolanaTransactionInfo> result = {};
for (var transactionModel in splTokenTransactions) {
result[transactionModel.id] = SolanaTransactionInfo(
id: transactionModel.id,
to: transactionModel.to,
from: transactionModel.from,
blockTime: transactionModel.blockTime,
direction: transactionModel.isOutgoingTx
? TransactionDirection.outgoing
: TransactionDirection.incoming,
solAmount: transactionModel.amount,
isPending: false,
txFee: transactionModel.fee,
tokenSymbol: transactionModel.tokenSymbol,
);
}
transactionHistory.addMany(result);
await transactionHistory.save();
}
@override
Future<void> rescan({required int height}) => throw UnimplementedError("rescan");
@override
Future<void> save() async {
await walletAddresses.updateAddressesInBox();
final path = await makePath();
await write(path: path, password: _password, data: toJSON());
await transactionHistory.save();
}
@action
@override
Future<void> startSync() async {
try {
syncStatus = AttemptingSyncStatus();
await Future.wait([
_updateBalance(),
_updateNativeSOLTransactions(),
_updateSPLTokenTransactions(),
]);
syncStatus = SyncedSyncStatus();
} catch (e) {
syncStatus = FailedSyncStatus();
}
}
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
String toJSON() => json.encode({
'mnemonic': _mnemonic,
'private_key': privateKey,
'balance': balance[currency]!.toJSON(),
});
static Future<SolanaWallet> open({
required String name,
required String password,
required WalletInfo walletInfo,
}) async {
final path = await pathForWallet(name: name, type: walletInfo.type);
final jsonSource = await read(path: path, password: password);
final data = json.decode(jsonSource) as Map;
final mnemonic = data['mnemonic'] as String?;
final privateKey = data['private_key'] as String?;
final balance = SolanaBalance.fromJSON(data['balance'] as String) ?? SolanaBalance(0.0);
return SolanaWallet(
walletInfo: walletInfo,
password: password,
mnemonic: mnemonic,
privateKey: privateKey,
initialBalance: balance,
);
}
Future<void> _updateBalance() async {
balance[currency] = await _fetchSOLBalance();
await _fetchSPLTokensBalances();
await save();
}
Future<SolanaBalance> _fetchSOLBalance() async {
final balance = await _client.getBalance(_walletKeyPair!.address);
return SolanaBalance(balance);
}
Future<void> _fetchSPLTokensBalances() async {
for (var token in splTokensBox.values) {
if (token.enabled) {
try {
final tokenBalance =
await _client.getSplTokenBalance(token.mintAddress, _walletKeyPair!.address) ??
balance[token] ??
SolanaBalance(0.0);
balance[token] = tokenBalance;
} catch (e) {
print('Error fetching spl token (${token.symbol}) balance ${e.toString()}');
}
} else {
balance.remove(token);
}
}
}
@override
Future<void>? updateBalance() async => await _updateBalance();
List<SPLToken> get splTokenCurrencies => splTokensBox.values.toList();
void addInitialTokens() {
final initialSPLTokens = DefaultSPLTokens().initialSPLTokens;
for (var token in initialSPLTokens) {
splTokensBox.put(token.mintAddress, token);
}
}
Future<void> addSPLToken(SPLToken token) async {
await splTokensBox.put(token.mintAddress, token);
if (token.enabled) {
final tokenBalance =
await _client.getSplTokenBalance(token.mintAddress, _walletKeyPair!.address) ??
balance[token] ??
SolanaBalance(0.0);
balance[token] = tokenBalance;
} else {
balance.remove(token);
}
}
Future<void> deleteSPLToken(SPLToken token) async {
await token.delete();
balance.remove(token);
_updateBalance();
}
Future<SPLToken?> getSPLToken(String mintAddress) async {
// Convert SPL token mint address to public key
final mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress);
// Fetch token's metadata account
final token = await solanaClient!.rpcClient.getMetadata(mint: mintPublicKey);
if (token == null) {
return null;
}
return SPLToken.fromMetadata(
name: token.name,
mint: token.mint,
symbol: token.symbol,
mintAddress: mintAddress,
);
}
@override
Future<void> renameWalletFiles(String newWalletName) async {
final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
final currentWalletFile = File(currentWalletPath);
final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName');
// Copies current wallet files into new wallet name's dir and files
if (currentWalletFile.existsSync()) {
final newWalletPath = await pathForWallet(name: newWalletName, type: type);
await currentWalletFile.copy(newWalletPath);
}
if (currentTransactionsFile.existsSync()) {
final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName');
}
// Delete old name's dir and files
await Directory(currentDirPath).delete(recursive: true);
}
void _setTransactionUpdateTimer() {
if (_transactionsUpdateTimer?.isActive ?? false) {
_transactionsUpdateTimer!.cancel();
}
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) {
_updateSPLTokenTransactions();
_updateNativeSOLTransactions();
_updateBalance();
});
}
Future<String> signSolanaMessage(String message) async {
// Convert the message to bytes
final messageBytes = utf8.encode(message);
// Sign the message bytes with the wallet's private key
final signature = await _walletKeyPair!.sign(messageBytes);
// Convert the signature to a hexadecimal string
final hex = bytesToHex(signature.bytes);
return hex;
}
SolanaClient? get solanaClient => _client.getSolanaClient;
}

View file

@ -0,0 +1,33 @@
import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:mobx/mobx.dart';
part 'solana_wallet_addresses.g.dart';
class SolanaWalletAddresses = SolanaWalletAddressesBase with _$SolanaWalletAddresses;
abstract class SolanaWalletAddressesBase extends WalletAddresses with Store {
SolanaWalletAddressesBase(WalletInfo walletInfo)
: address = '',
super(walletInfo);
@override
String address;
@override
Future<void> init() async {
address = walletInfo.address;
await updateAddressesInBox();
}
@override
Future<void> updateAddressesInBox() async {
try {
addressesMap.clear();
addressesMap[address] = '';
await saveAddressesInBox();
} catch (e) {
print(e.toString());
}
}
}

View file

@ -0,0 +1,29 @@
import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
class SolanaNewWalletCredentials extends WalletCredentials {
SolanaNewWalletCredentials({required String name, WalletInfo? walletInfo})
: super(name: name, walletInfo: walletInfo);
}
class SolanaRestoreWalletFromSeedCredentials extends WalletCredentials {
SolanaRestoreWalletFromSeedCredentials(
{required String name,
required String password,
required this.mnemonic,
WalletInfo? walletInfo})
: super(name: name, password: password, walletInfo: walletInfo);
final String mnemonic;
}
class SolanaRestoreWalletFromPrivateKey extends WalletCredentials {
SolanaRestoreWalletFromPrivateKey(
{required String name,
required String password,
required this.privateKey,
WalletInfo? walletInfo})
: super(name: name, password: password, walletInfo: walletInfo);
final String privateKey;
}

View file

@ -0,0 +1,118 @@
import 'dart:io';
import 'package:bip39/bip39.dart' as bip39;
import 'package:collection/collection.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_solana/solana_mnemonics.dart';
import 'package:cw_solana/solana_wallet.dart';
import 'package:cw_solana/solana_wallet_creation_credentials.dart';
import 'package:hive/hive.dart';
class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
SolanaRestoreWalletFromSeedCredentials, SolanaRestoreWalletFromPrivateKey> {
SolanaWalletService(this.walletInfoSource);
final Box<WalletInfo> walletInfoSource;
@override
Future<SolanaWallet> create(SolanaNewWalletCredentials credentials) async {
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
final mnemonic = bip39.generateMnemonic(strength: strength);
final wallet = SolanaWallet(
walletInfo: credentials.walletInfo!,
mnemonic: mnemonic,
password: credentials.password!,
);
await wallet.init();
wallet.addInitialTokens();
return wallet;
}
@override
WalletType getType() => WalletType.solana;
@override
Future<bool> isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: getType())).existsSync();
@override
Future<SolanaWallet> openWallet(String name, String password) async {
final walletInfo =
walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
final wallet = await SolanaWalletBase.open(
name: name,
password: password,
walletInfo: walletInfo,
);
await wallet.init();
await wallet.save();
return wallet;
}
@override
Future<void> remove(String wallet) async {
File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
final walletInfo = walletInfoSource.values
.firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
await walletInfoSource.delete(walletInfo.key);
}
@override
Future<SolanaWallet> restoreFromKeys(SolanaRestoreWalletFromPrivateKey credentials) async {
final wallet = SolanaWallet(
password: credentials.password!,
privateKey: credentials.privateKey,
walletInfo: credentials.walletInfo!,
);
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}
@override
Future<SolanaWallet> restoreFromSeed(SolanaRestoreWalletFromSeedCredentials credentials) async {
if (!bip39.validateMnemonic(credentials.mnemonic)) {
throw SolanaMnemonicIsIncorrectException();
}
final wallet = SolanaWallet(
password: credentials.password!,
mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!,
);
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}
@override
Future<void> rename(String currentName, String password, String newName) async {
final currentWalletInfo = walletInfoSource.values
.firstWhere((info) => info.id == WalletBase.idFor(currentName, getType()));
final currentWallet = await SolanaWalletBase.open(
password: password, name: currentName, walletInfo: currentWalletInfo);
await currentWallet.renameWalletFiles(newName);
final newWalletInfo = currentWalletInfo;
newWalletInfo.id = WalletBase.idFor(newName, getType());
newWalletInfo.name = newName;
await walletInfoSource.put(currentWalletInfo.key, newWalletInfo);
}
}

View file

@ -0,0 +1,146 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/hive_type_ids.dart';
import 'package:hive/hive.dart';
import 'package:solana/metaplex.dart';
part 'spl_token.g.dart';
@HiveType(typeId: SPLToken.typeId)
class SPLToken extends CryptoCurrency with HiveObjectMixin {
@HiveField(0)
final String name;
@HiveField(1)
final String symbol;
@HiveField(2)
final String mintAddress;
@HiveField(3)
final int decimal;
@HiveField(4, defaultValue: false)
bool _enabled;
@HiveField(5)
final String mint;
@HiveField(6)
final String? iconPath;
@HiveField(7)
final String? tag;
SPLToken({
required this.name,
required this.symbol,
required this.mintAddress,
required this.decimal,
required this.mint,
this.iconPath,
this.tag = 'SOL',
bool enabled = false,
}) : _enabled = enabled,
super(
name: mint.toLowerCase(),
title: symbol.toUpperCase(),
fullName: name,
tag: tag,
iconPath: iconPath,
decimals: decimal,
);
factory SPLToken.fromMetadata({
required String name,
required String mint,
required String symbol,
required String mintAddress,
}) {
return SPLToken(
name: name,
symbol: symbol,
mintAddress: mintAddress,
decimal: 0,
mint: mint,
iconPath: '',
);
}
factory SPLToken.cryptoCurrency({
required String name,
required String symbol,
required int decimals,
required String iconPath,
required String mint,
}) {
return SPLToken(
name: name,
symbol: symbol,
decimal: decimals,
mint: mint,
iconPath: iconPath,
mintAddress: '',
);
}
bool get enabled => _enabled;
set enabled(bool value) => _enabled = value;
SPLToken.copyWith(SPLToken other, String? icon, String? tag)
: name = other.name,
symbol = other.symbol,
mintAddress = other.mintAddress,
decimal = other.decimal,
_enabled = other.enabled,
mint = other.mint,
tag = other.tag,
iconPath = icon,
super(
title: other.symbol.toUpperCase(),
name: other.symbol.toLowerCase(),
decimals: other.decimal,
fullName: other.name,
tag: other.tag,
iconPath: icon,
);
static const typeId = SPL_TOKEN_TYPE_ID;
static const boxName = 'SPLTokens';
@override
bool operator ==(other) =>
(other is SPLToken && other.mintAddress == mintAddress) ||
(other is CryptoCurrency && other.title == title);
@override
int get hashCode => mintAddress.hashCode;
}
class NFT extends SPLToken {
final ImageInfo? imageInfo;
NFT(
String mint,
String name,
String symbol,
String mintAddress,
int decimal,
String iconPath,
this.imageInfo,
) : super(
name: name,
symbol: symbol,
mintAddress: mintAddress,
decimal: decimal,
mint: mint,
iconPath: iconPath,
);
}
class ImageInfo {
final String uri;
final OffChainMetadata? data;
const ImageInfo(this.uri, this.data);
}

37
cw_solana/pubspec.yaml Normal file
View file

@ -0,0 +1,37 @@
name: cw_solana
description: A new Flutter package project.
version: 0.0.1
publish_to: none
homepage: https://cakewallet.com
environment:
sdk: '>=3.0.6 <4.0.0'
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
solana: ^0.30.1
cw_core:
path: ../cw_core
http: ^1.1.0
hive: ^2.2.3
bip39: ^1.0.6
mobx: ^2.3.0+1
shared_preferences: ^2.0.15
web3dart: ^2.7.1
bip32: ^2.0.0
hex: ^0.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
build_runner: ^2.1.11
mobx_codegen: ^2.0.7
hive_generator: ^1.1.3
flutter:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg

View file

@ -0,0 +1,12 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:cw_solana/cw_solana.dart';
void main() {
test('adds one to input values', () {
final calculator = Calculator();
expect(calculator.addOne(2), 3);
expect(calculator.addOne(-7), -6);
expect(calculator.addOne(0), 1);
});
}

View file

@ -180,6 +180,16 @@
<string>polygon-wallet</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>solana-wallet</string>
<key>CFBundleURLSchemes</key>
<array>
<string>solana-wallet</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>

View file

@ -1,6 +1,7 @@
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/core/validator.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
@ -8,9 +9,7 @@ class AddressValidator extends TextValidator {
AddressValidator({required CryptoCurrency type})
: super(
errorMessage: S.current.error_text_address,
useAdditionalValidation: type == CryptoCurrency.btc
? bitcoin.Address.validateAddress
: null,
useAdditionalValidation: type == CryptoCurrency.btc ? bitcoin.Address.validateAddress : null,
pattern: getPattern(type),
length: getLength(type));
@ -130,6 +129,12 @@ class AddressValidator extends TextValidator {
if (type is Erc20Token) {
return [42];
}
if (solana != null) {
final length = solana!.getValidationLength(type);
if (length != null) return length;
}
switch (type) {
case CryptoCurrency.xmr:
return null;
@ -192,11 +197,11 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.sc:
return [76];
case CryptoCurrency.sol:
case CryptoCurrency.usdtSol:
case CryptoCurrency.usdcsol:
return [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44];
case CryptoCurrency.trx:
return [34];
case CryptoCurrency.usdcsol:
return [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44];
case CryptoCurrency.usdt:
return [34];
case CryptoCurrency.usdttrc20:
@ -286,6 +291,8 @@ class AddressValidator extends TextValidator {
'|bitcoincash:q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{41}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)';
case CryptoCurrency.sol:
return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)';
default:
return null;
}

View file

@ -4,6 +4,7 @@ import 'package:cake_wallet/haven/haven.dart';
import 'package:cake_wallet/core/validator.dart';
import 'package:cake_wallet/entities/mnemonic_item.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/nano/nano.dart';
@ -37,6 +38,8 @@ class SeedValidator extends Validator<MnemonicItem> {
return nano!.getNanoWordList(language);
case WalletType.polygon:
return polygon!.getPolygonWordList(language);
case WalletType.solana:
return solana!.getSolanaWordList(language);
default:
return [];
}

View file

@ -1,4 +1,4 @@
import 'package:cake_wallet/core/wallet_connect/evm_chain_service.dart';
import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_service.dart';
enum EVMChainId {
ethereum,

View file

@ -3,7 +3,7 @@ import 'dart:developer';
import 'dart:typed_data';
import 'package:cake_wallet/core/wallet_connect/eth_transaction_model.dart';
import 'package:cake_wallet/core/wallet_connect/evm_chain_id.dart';
import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
@ -20,8 +20,8 @@ import 'package:eth_sig_util/util/utils.dart';
import 'package:http/http.dart' as http;
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
import 'package:web3dart/web3dart.dart';
import 'chain_service.dart';
import 'wallet_connect_key_service.dart';
import '../chain_service.dart';
import '../../wallet_connect_key_service.dart';
class EvmChainServiceImpl implements ChainService {
final AppStore appStore;

View file

@ -0,0 +1,28 @@
class SolanaSignMessage {
final String pubkey;
final String message;
SolanaSignMessage({
required this.pubkey,
required this.message,
});
factory SolanaSignMessage.fromJson(Map<String, dynamic> json) {
return SolanaSignMessage(
pubkey: json['pubkey'] as String,
message: json['message'] as String,
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'pubkey': pubkey,
'message': message,
};
}
@override
String toString() {
return 'SolanaSignMessage(pubkey: $pubkey, message: $message)';
}
}

View file

@ -0,0 +1,106 @@
class SolanaSignTransaction {
final String? feePayer;
final String? recentBlockhash;
final String transaction;
final List<SolanaInstruction>? instructions;
SolanaSignTransaction({
required this.feePayer,
required this.recentBlockhash,
required this.instructions,
required this.transaction,
});
factory SolanaSignTransaction.fromJson(Map<String, dynamic> json) {
return SolanaSignTransaction(
feePayer:json['feePayer'] !=null ? json['feePayer'] as String: null,
recentBlockhash: json['recentBlockhash']!=null? json['recentBlockhash'] as String: null,
instructions:json['instructions']!=null? (json['instructions'] as List<dynamic>)
.map((e) => SolanaInstruction.fromJson(e as Map<String, dynamic>))
.toList(): null,
transaction: json['transaction'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'feePayer': feePayer,
'recentBlockhash': recentBlockhash,
'instructions': instructions,
'transaction': transaction,
};
}
@override
String toString() {
return 'SolanaSignTransaction(feePayer: $feePayer, recentBlockhash: $recentBlockhash, instructions: $instructions, transaction: $transaction)';
}
}
class SolanaInstruction {
final String programId;
final List<SolanaKeyMetadata> keys;
final List<int> data;
SolanaInstruction({
required this.programId,
required this.keys,
required this.data,
});
factory SolanaInstruction.fromJson(Map<String, dynamic> json) {
return SolanaInstruction(
programId: json['programId'] as String,
keys: (json['keys'] as List<dynamic>)
.map((e) => SolanaKeyMetadata.fromJson(e as Map<String, dynamic>))
.toList(),
data: (json['data'] as List<dynamic>).map((e) => e as int).toList(),
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'programId': programId,
'keys': keys,
'data': data,
};
}
@override
String toString() {
return 'SolanaInstruction(programId: $programId, keys: $keys, data: $data)';
}
}
class SolanaKeyMetadata {
final String pubkey;
final bool isSigner;
final bool isWritable;
SolanaKeyMetadata({
required this.pubkey,
required this.isSigner,
required this.isWritable,
});
factory SolanaKeyMetadata.fromJson(Map<String, dynamic> json) {
return SolanaKeyMetadata(
pubkey: json['pubkey'] as String,
isSigner: json['isSigner'] as bool,
isWritable: json['isWritable'] as bool,
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'pubkey': pubkey,
'isSigner': isSigner,
'isWritable': isWritable,
};
}
@override
String toString() {
return 'SolanaKeyMetadata(pubkey: $pubkey, isSigner: $isSigner, isWritable: $isWritable)';
}
}

View file

@ -0,0 +1,27 @@
import 'solana_chain_service.dart';
enum SolanaChainId {
mainnet,
testnet,
devnet,
}
extension SolanaChainIdX on SolanaChainId {
String chain() {
String name = '';
switch (this) {
case SolanaChainId.mainnet:
name = '4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ';
break;
case SolanaChainId.testnet:
name = '8E9rvCKLFQia2Y35HXjjpWzj8weVo44K';
break;
case SolanaChainId.devnet:
name = '';
break;
}
return '${SolanaChainServiceImpl.namespace}:$name';
}
}

View file

@ -0,0 +1,177 @@
import 'dart:developer';
import 'package:cake_wallet/core/wallet_connect/chain_service/solana/entities/solana_sign_message.dart';
import 'package:cake_wallet/core/wallet_connect/chain_service/solana/solana_chain_id.dart';
import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart';
import 'package:cake_wallet/core/wallet_connect/models/connection_model.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart';
import 'package:solana/base58.dart';
import 'package:solana/solana.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
import '../chain_service.dart';
import '../../wallet_connect_key_service.dart';
import 'entities/solana_sign_transaction.dart';
class SolanaChainServiceImpl implements ChainService {
final BottomSheetService bottomSheetService;
final Web3Wallet wallet;
final WalletConnectKeyService wcKeyService;
static const namespace = 'solana';
static const solSignTransaction = 'solana_signTransaction';
static const solSignMessage = 'solana_signMessage';
final SolanaChainId reference;
final SolanaClient solanaClient;
final Ed25519HDKeyPair? ownerKeyPair;
SolanaChainServiceImpl({
required this.reference,
required this.wcKeyService,
required this.bottomSheetService,
required this.wallet,
required this.ownerKeyPair,
required String webSocketUrl,
required Uri rpcUrl,
SolanaClient? solanaClient,
}) : solanaClient = solanaClient ??
SolanaClient(
rpcUrl: rpcUrl,
websocketUrl: Uri.parse(webSocketUrl),
timeout: const Duration(minutes: 2),
) {
for (final String event in getEvents()) {
wallet.registerEventEmitter(chainId: getChainId(), event: event);
}
wallet.registerRequestHandler(
chainId: getChainId(),
method: solSignTransaction,
handler: solanaSignTransaction,
);
wallet.registerRequestHandler(
chainId: getChainId(),
method: solSignMessage,
handler: solanaSignMessage,
);
}
@override
String getNamespace() {
return namespace;
}
@override
String getChainId() {
return reference.chain();
}
@override
List<String> getEvents() {
return [''];
}
Future<String?> requestAuthorization(String? text) async {
// Show the bottom sheet
final bool? isApproved = await bottomSheetService.queueBottomSheet(
widget: Web3RequestModal(
child: ConnectionWidget(
title: S.current.signTransaction,
info: [
ConnectionModel(
text: text,
),
],
),
),
) as bool?;
if (isApproved != null && isApproved == false) {
return 'User rejected signature';
}
return null;
}
Future<String> solanaSignTransaction(String topic, dynamic parameters) async {
log('received solana sign transaction request $parameters');
final solanaSignTx =
SolanaSignTransaction.fromJson(parameters as Map<String, dynamic>);
final String? authError = await requestAuthorization('Confirm request to sign transaction?');
if (authError != null) {
return authError;
}
try {
final message =
await solanaClient.rpcClient.getMessageFromEncodedTx(solanaSignTx.transaction);
final sign = await ownerKeyPair?.signMessage(
message: message,
recentBlockhash: solanaSignTx.recentBlockhash ?? '',
);
if (sign == null) {
return '';
}
String signature = sign.signatures.first.toBase58();
print(signature);
print(signature.runtimeType);
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: S.current.awaitDAppProcessing,
isError: false,
),
);
return signature;
} catch (e) {
log('An error has occurred while signing transaction: ${e.toString()}');
bottomSheetService.queueBottomSheet(
isModalDismissible: true,
widget: BottomSheetMessageDisplayWidget(
message: '${S.current.errorSigningTransaction}: ${e.toString()}',
),
);
return 'Failed';
}
}
Future<String> solanaSignMessage(String topic, dynamic parameters) async {
log('received solana sign message request: $parameters');
final solanaSignMessage = SolanaSignMessage.fromJson(parameters as Map<String, dynamic>);
final String? authError = await requestAuthorization('Confirm request to sign message?');
if (authError != null) {
return authError;
}
Signature? sign;
try {
sign = await ownerKeyPair?.sign(base58decode(solanaSignMessage.message));
} catch (e) {
print(e);
}
if (sign == null) {
return '';
}
String signature = sign.toBase58();
return signature;
}
}

View file

@ -2,6 +2,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
@ -13,7 +14,6 @@ abstract class WalletConnectKeyService {
/// If the chain is not found, returns an empty list.
/// - [chain]: The chain to get the keys for.
List<ChainKeyModel> getKeysForChain(WalletBase wallet);
}
class KeyServiceImpl implements WalletConnectKeyService {
@ -23,6 +23,8 @@ class KeyServiceImpl implements WalletConnectKeyService {
return ethereum!.getPrivateKey(wallet);
case WalletType.polygon:
return polygon!.getPrivateKey(wallet);
case WalletType.solana:
return solana!.getPrivateKey(wallet);
default:
return '';
}
@ -34,6 +36,8 @@ class KeyServiceImpl implements WalletConnectKeyService {
return ethereum!.getPublicKey(wallet);
case WalletType.polygon:
return polygon!.getPublicKey(wallet);
case WalletType.solana:
return solana!.getPublicKey(wallet);
default:
return '';
}
@ -53,6 +57,14 @@ class KeyServiceImpl implements WalletConnectKeyService {
privateKey: _getPrivateKeyForWallet(wallet),
publicKey: _getPublicKeyForWallet(wallet),
),
ChainKeyModel(
chains: [
'solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ', // main-net
'solana:8E9rvCKLFQia2Y35HXjjpWzj8weVo44K', // test-net
],
privateKey: _getPrivateKeyForWallet(wallet),
publicKey: _getPublicKeyForWallet(wallet),
),
];
return keys;
}

View file

@ -2,23 +2,27 @@ import 'dart:async';
import 'dart:developer';
import 'dart:typed_data';
import 'package:cake_wallet/core/wallet_connect/evm_chain_id.dart';
import 'package:cake_wallet/core/wallet_connect/evm_chain_service.dart';
import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart';
import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_service.dart';
import 'package:cake_wallet/core/wallet_connect/wallet_connect_key_service.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/core/wallet_connect/models/auth_request_model.dart';
import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart';
import 'package:cake_wallet/core/wallet_connect/models/session_request_model.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_request_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:eth_sig_util/eth_sig_util.dart';
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
import 'chain_service/solana/solana_chain_id.dart';
import 'chain_service/solana/solana_chain_service.dart';
import 'wc_bottom_sheet_service.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
@ -114,14 +118,34 @@ abstract class Web3WalletServiceBase with Store {
final newAuthRequests = _web3Wallet.completeRequests.getAll();
auth.addAll(newAuthRequests);
for (final cId in EVMChainId.values) {
EvmChainServiceImpl(
reference: cId,
appStore: appStore,
wcKeyService: walletKeyService,
bottomSheetService: _bottomSheetHandler,
wallet: _web3Wallet,
);
if (isEVMCompatibleChain(appStore.wallet!.type)) {
for (final cId in EVMChainId.values) {
EvmChainServiceImpl(
reference: cId,
appStore: appStore,
wcKeyService: walletKeyService,
bottomSheetService: _bottomSheetHandler,
wallet: _web3Wallet,
);
}
}
if (appStore.wallet!.type == WalletType.solana) {
for (final cId in SolanaChainId.values) {
final node = appStore.settingsStore.getCurrentNode(appStore.wallet!.type);
final rpcUri = node.uri;
final webSocketUri = 'wss://${node.uriRaw}/ws${node.uri.path}';
SolanaChainServiceImpl(
reference: cId,
rpcUrl: rpcUri,
webSocketUrl: webSocketUri,
wcKeyService: walletKeyService,
bottomSheetService: _bottomSheetHandler,
wallet: _web3Wallet,
ownerKeyPair: solana!.getWalletKeyPair(appStore.wallet!),
);
}
}
}

View file

@ -21,6 +21,7 @@ import 'package:cake_wallet/ionia/ionia_gift_card.dart';
import 'package:cake_wallet/ionia/ionia_tip.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dart';
import 'package:cake_wallet/src/screens/buy/buy_options_page.dart';
import 'package:cake_wallet/src/screens/buy/webview_page.dart';
@ -863,6 +864,8 @@ Future<void> setup({
return nano!.createNanoWalletService(_walletInfoSource);
case WalletType.polygon:
return polygon!.createPolygonWalletService(_walletInfoSource);
case WalletType.solana:
return solana!.createSolanaWalletService(_walletInfoSource);
default:
throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService');
}
@ -1174,7 +1177,7 @@ Future<void> setup({
getIt.registerFactoryParam<EditTokenPage, HomeSettingsViewModel, Map<String, dynamic>>(
(homeSettingsViewModel, arguments) => EditTokenPage(
homeSettingsViewModel: homeSettingsViewModel,
erc20token: arguments['token'] as Erc20Token?,
token: arguments['token'] as CryptoCurrency?,
initialContractAddress: arguments['contractAddress'] as String?,
),
);

View file

@ -30,6 +30,7 @@ const polygonDefaultNodeUri = 'polygon-bor.publicnode.com';
const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002';
const nanoDefaultNodeUri = 'rpc.nano.to';
const nanoDefaultPowNodeUri = 'rpc.nano.to';
const solanaDefaultNodeUri = 'rpc.ankr.com';
Future<void> defaultSettingsMigration(
{required int version,
@ -186,10 +187,15 @@ Future<void> defaultSettingsMigration(
await rewriteSecureStoragePin(secureStorage: secureStorage);
break;
case 26:
/// commented out as it was a probable cause for some users to have white screen issues
/// maybe due to multiple access on Secure Storage at once
/// or long await time on start of the app
// await insecureStorageMigration(secureStorage: secureStorage, sharedPreferences: sharedPreferences);
/// commented out as it was a probable cause for some users to have white screen issues
/// maybe due to multiple access on Secure Storage at once
/// or long await time on start of the app
// await insecureStorageMigration(secureStorage: secureStorage, sharedPreferences: sharedPreferences);
case 27:
await addSolanaNodeList(nodes: nodes);
await changeSolanaCurrentNodeToDefault(
sharedPreferences: sharedPreferences, nodes: nodes);
break;
default:
break;
@ -384,6 +390,11 @@ Node getMoneroDefaultNode({required Box<Node> nodes}) {
}
}
Node? getSolanaDefaultNode({required Box<Node> nodes}) {
return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == solanaDefaultNodeUri) ??
nodes.values.firstWhereOrNull((node) => node.type == WalletType.solana);
}
Future<void> insecureStorageMigration({
required SharedPreferences sharedPreferences,
required FlutterSecureStorage secureStorage,
@ -673,6 +684,7 @@ Future<void> checkCurrentNodes(
final currentNanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey);
final currentBitcoinCashNodeId =
sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey);
final currentSolanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey);
final currentMoneroNode =
nodeSource.values.firstWhereOrNull((node) => node.key == currentMoneroNodeId);
final currentBitcoinElectrumServer =
@ -691,6 +703,8 @@ Future<void> checkCurrentNodes(
powNodeSource.values.firstWhereOrNull((node) => node.key == currentNanoPowNodeId);
final currentBitcoinCashNodeServer =
nodeSource.values.firstWhereOrNull((node) => node.key == currentBitcoinCashNodeId);
final currentSolanaNodeServer =
nodeSource.values.firstWhereOrNull((node) => node.key == currentSolanaNodeId);
if (currentMoneroNode == null) {
final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero);
await nodeSource.add(newCakeWalletNode);
@ -750,6 +764,12 @@ Future<void> checkCurrentNodes(
await nodeSource.add(node);
await sharedPreferences.setInt(PreferencesKey.currentPolygonNodeIdKey, node.key as int);
}
if (currentSolanaNodeServer == null) {
final node = Node(uri: solanaDefaultNodeUri, type: WalletType.solana);
await nodeSource.add(node);
await sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, node.key as int);
}
}
Future<void> resetBitcoinElectrumServer(
@ -861,3 +881,20 @@ Future<void> changePolygonCurrentNodeToDefault(
await sharedPreferences.setInt(PreferencesKey.currentPolygonNodeIdKey, nodeId);
}
Future<void> addSolanaNodeList({required Box<Node> nodes}) async {
final nodeList = await loadDefaultSolanaNodes();
for (var node in nodeList) {
if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) {
await nodes.add(node);
}
}
}
Future<void> changeSolanaCurrentNodeToDefault(
{required SharedPreferences sharedPreferences, required Box<Node> nodes}) async {
final node = getSolanaDefaultNode(nodes: nodes);
final nodeId = node?.key as int? ?? 0;
await sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, nodeId);
}

View file

@ -149,6 +149,23 @@ Future<List<Node>> loadDefaultPolygonNodes() async {
return nodes;
}
Future<List<Node>> loadDefaultSolanaNodes() async {
final nodesRaw = await rootBundle.loadString('assets/solana_node_list.yml');
final loadedNodes = loadYaml(nodesRaw) as YamlList;
final nodes = <Node>[];
for (final raw in loadedNodes) {
if (raw is Map) {
final node = Node.fromMap(Map<String, Object>.from(raw));
node.type = WalletType.solana;
nodes.add(node);
}
}
return nodes;
}
Future<void> resetToDefault(Box<Node> nodeSource) async {
final moneroNodes = await loadDefaultNodes();
final bitcoinElectrumServerList = await loadBitcoinElectrumServerList();
@ -158,6 +175,7 @@ Future<void> resetToDefault(Box<Node> nodeSource) async {
final ethereumNodes = await loadDefaultEthereumNodes();
final nanoNodes = await loadDefaultNanoNodes();
final polygonNodes = await loadDefaultPolygonNodes();
final solanaNodes = await loadDefaultSolanaNodes();
final nodes = moneroNodes +
bitcoinElectrumServerList +
@ -166,7 +184,8 @@ Future<void> resetToDefault(Box<Node> nodeSource) async {
ethereumNodes +
bitcoinCashElectrumServerList +
nanoNodes +
polygonNodes;
polygonNodes +
solanaNodes;
await nodeSource.clear();
await nodeSource.addAll(nodes);

View file

@ -13,6 +13,7 @@ class PreferencesKey {
static const currentBananoPowNodeIdKey = 'current_node_id_banano_pow';
static const currentFiatCurrencyKey = 'current_fiat_currency';
static const currentBitcoinCashNodeIdKey = 'current_node_id_bch';
static const currentSolanaNodeIdKey = 'current_node_id_sol';
static const currentTransactionPriorityKeyLegacy = 'current_fee_priority';
static const currentBalanceDisplayModeKey = 'current_balance_display_mode';
static const shouldSaveRecipientAddressKey = 'save_recipient_address';

View file

@ -21,12 +21,13 @@ List<TransactionPriority> priorityForWalletType(WalletType type) {
return ethereum!.getTransactionPriorities();
case WalletType.bitcoinCash:
return bitcoinCash!.getTransactionPriorities();
// no such thing for nano/banano:
case WalletType.nano:
case WalletType.banano:
return [];
case WalletType.polygon:
return polygon!.getTransactionPriorities();
// no such thing for nano/banano/solana:
case WalletType.nano:
case WalletType.banano:
case WalletType.solana:
return [];
default:
return [];
}

View file

@ -67,6 +67,8 @@ class ProvidersHelper {
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
case WalletType.polygon:
return [ProviderType.askEachTime, ProviderType.dfx];
case WalletType.solana:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
case WalletType.none:
case WalletType.haven:
return [];
@ -88,6 +90,13 @@ class ProvidersHelper {
return [ProviderType.askEachTime, ProviderType.moonpaySell];
case WalletType.polygon:
return [ProviderType.askEachTime, ProviderType.dfx];
case WalletType.solana:
return [
ProviderType.askEachTime,
ProviderType.onramper,
ProviderType.robinhood,
ProviderType.moonpaySell,
];
case WalletType.monero:
case WalletType.nano:
case WalletType.banano:

View file

@ -119,12 +119,13 @@ class CWEthereum extends Ethereum {
}
@override
Future<void> addErc20Token(WalletBase wallet, Erc20Token token) async =>
await (wallet as EthereumWallet).addErc20Token(token);
Future<void> addErc20Token(WalletBase wallet, CryptoCurrency token) async {
await (wallet as EthereumWallet).addErc20Token(token as Erc20Token);
}
@override
Future<void> deleteErc20Token(WalletBase wallet, Erc20Token token) async =>
await (wallet as EthereumWallet).deleteErc20Token(token);
Future<void> deleteErc20Token(WalletBase wallet, CryptoCurrency token) async =>
await (wallet as EthereumWallet).deleteErc20Token(token as Erc20Token);
@override
Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress) async {
@ -153,4 +154,6 @@ class CWEthereum extends Ethereum {
Web3Client? getWeb3Client(WalletBase wallet) {
return (wallet as EthereumWallet).getWeb3Client();
}
String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress;
}

View file

@ -163,11 +163,11 @@ Future<void> initializeAppConfigs() async {
transactionDescriptions: transactionDescriptions,
secureStorage: secureStorage,
anonpayInvoiceInfo: anonpayInvoiceInfo,
initialMigrationVersion: 26);
initialMigrationVersion: 27);
}
Future<void> initialSetup(
{required SharedPreferences sharedPreferences,
{required SharedPreferences sharedPreferences,
required Box<Node> nodes,
required Box<Node> powNodes,
required Box<WalletInfo> walletInfoSource,

View file

@ -119,12 +119,12 @@ class CWPolygon extends Polygon {
}
@override
Future<void> addErc20Token(WalletBase wallet, Erc20Token token) async =>
await (wallet as PolygonWallet).addErc20Token(token);
Future<void> addErc20Token(WalletBase wallet, CryptoCurrency token) async =>
await (wallet as PolygonWallet).addErc20Token(token as Erc20Token);
@override
Future<void> deleteErc20Token(WalletBase wallet, Erc20Token token) async =>
await (wallet as PolygonWallet).deleteErc20Token(token);
Future<void> deleteErc20Token(WalletBase wallet, CryptoCurrency token) async =>
await (wallet as PolygonWallet).deleteErc20Token(token as Erc20Token);
@override
Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress) async {
@ -153,4 +153,6 @@ class CWPolygon extends Polygon {
Web3Client? getWeb3Client(WalletBase wallet) {
return (wallet as PolygonWallet).getWeb3Client();
}
String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress;
}

View file

@ -4,9 +4,11 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/entities/update_haven_rate.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
@ -35,7 +37,7 @@ Future<void> startFiatRateUpdate(
torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly);
}
Iterable<Erc20Token>? currencies;
Iterable<CryptoCurrency>? currencies;
if (appStore.wallet!.type == WalletType.ethereum) {
currencies =
ethereum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled);
@ -46,6 +48,12 @@ Future<void> startFiatRateUpdate(
polygon!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled);
}
if (appStore.wallet!.type == WalletType.solana) {
currencies =
solana!.getSPLTokenCurrencies(appStore.wallet!).where((element) => element.enabled);
}
if (currencies != null) {
for (final currency in currencies) {
() async {

View file

@ -3,6 +3,8 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/entities/update_haven_rate.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/balance.dart';
@ -109,7 +111,7 @@ void startCurrentWalletChangeReaction(
fiat: settingsStore.fiatCurrency,
torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly);
Iterable<Erc20Token>? currencies;
Iterable<CryptoCurrency>? currencies;
if (wallet.type == WalletType.ethereum) {
currencies =
ethereum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled);
@ -118,7 +120,11 @@ void startCurrentWalletChangeReaction(
currencies =
polygon!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled);
}
if (wallet.type == WalletType.solana) {
currencies =
solana!.getSPLTokenCurrencies(appStore.wallet!).where((element) => element.enabled);
}
if (currencies != null) {
for (final currency in currencies) {
() async {

View file

@ -1,4 +1,5 @@
import 'package:cake_wallet/core/wallet_connect/evm_chain_id.dart';
import 'package:cake_wallet/core/wallet_connect/chain_service/eth/evm_chain_id.dart';
import 'package:cake_wallet/core/wallet_connect/chain_service/solana/solana_chain_id.dart';
import 'package:cw_core/wallet_type.dart';
bool isEVMCompatibleChain(WalletType walletType) {
@ -11,12 +12,24 @@ bool isEVMCompatibleChain(WalletType walletType) {
}
}
bool isWalletConnectCompatibleChain(WalletType walletType) {
switch (walletType) {
case WalletType.polygon:
case WalletType.ethereum:
return true;
default:
return false;
}
}
String getChainNameSpaceAndIdBasedOnWalletType(WalletType walletType) {
switch (walletType) {
case WalletType.ethereum:
return EVMChainId.ethereum.chain();
case WalletType.polygon:
return EVMChainId.polygon.chain();
case WalletType.solana:
return SolanaChainId.mainnet.chain();
default:
return '';
}
@ -40,6 +53,8 @@ String getChainNameBasedOnWalletType(WalletType walletType) {
return 'eth';
case WalletType.polygon:
return 'polygon';
case WalletType.solana:
return 'solana';
default:
return '';
}
@ -51,6 +66,8 @@ String getTokenNameBasedOnWalletType(WalletType walletType) {
return 'ETH';
case WalletType.polygon:
return 'MATIC';
case WalletType.solana:
return 'SOL';
default:
return '';
}

118
lib/solana/cw_solana.dart Normal file
View file

@ -0,0 +1,118 @@
part of 'solana.dart';
class CWSolana extends Solana {
@override
List<String> getSolanaWordList(String language) => SolanaMnemonics.englishWordlist;
WalletService createSolanaWalletService(Box<WalletInfo> walletInfoSource) =>
SolanaWalletService(walletInfoSource);
@override
WalletCredentials createSolanaNewWalletCredentials({
required String name,
WalletInfo? walletInfo,
}) =>
SolanaNewWalletCredentials(name: name, walletInfo: walletInfo);
@override
WalletCredentials createSolanaRestoreWalletFromSeedCredentials({
required String name,
required String mnemonic,
required String password,
}) =>
SolanaRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic);
@override
WalletCredentials createSolanaRestoreWalletFromPrivateKey({
required String name,
required String privateKey,
required String password,
}) =>
SolanaRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey);
@override
String getAddress(WalletBase wallet) => (wallet as SolanaWallet).walletAddresses.address;
@override
String getPrivateKey(WalletBase wallet) => (wallet as SolanaWallet).privateKey;
@override
String getPublicKey(WalletBase wallet) => (wallet as SolanaWallet).keys.publicKey.toBase58();
@override
Ed25519HDKeyPair? getWalletKeyPair(WalletBase wallet) => (wallet as SolanaWallet).walletKeyPair;
Object createSolanaTransactionCredentials(
List<Output> outputs, {
required CryptoCurrency currency,
}) =>
SolanaTransactionCredentials(
outputs
.map((out) => OutputInfo(
fiatAmount: out.fiatAmount,
cryptoAmount: out.cryptoAmount,
address: out.address,
note: out.note,
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount))
.toList(),
currency: currency,
);
Object createSolanaTransactionCredentialsRaw(
List<OutputInfo> outputs, {
required CryptoCurrency currency,
}) =>
SolanaTransactionCredentials(outputs, currency: currency);
@override
List<SPLToken> getSPLTokenCurrencies(WalletBase wallet) {
final solanaWallet = wallet as SolanaWallet;
return solanaWallet.splTokenCurrencies;
}
@override
Future<void> addSPLToken(WalletBase wallet, CryptoCurrency token) async =>
await (wallet as SolanaWallet).addSPLToken(token as SPLToken);
@override
Future<void> deleteSPLToken(WalletBase wallet, CryptoCurrency token) async =>
await (wallet as SolanaWallet).deleteSPLToken(token as SPLToken);
@override
Future<SPLToken?> getSPLToken(WalletBase wallet, String mintAddress) async {
final solanaWallet = wallet as SolanaWallet;
return await solanaWallet.getSPLToken(mintAddress);
}
@override
CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) {
transaction as SolanaTransactionInfo;
if (transaction.tokenSymbol == CryptoCurrency.sol.title) {
return CryptoCurrency.sol;
}
wallet as SolanaWallet;
return wallet.splTokenCurrencies
.firstWhere((element) => transaction.tokenSymbol == element.symbol);
}
@override
double getTransactionAmountRaw(TransactionInfo transactionInfo) {
return (transactionInfo as SolanaTransactionInfo).solAmount.toDouble();
}
@override
String getTokenAddress(CryptoCurrency asset) => (asset as SPLToken).mintAddress;
@override
List<int>? getValidationLength(CryptoCurrency type) {
if (type is SPLToken) {
return [44];
}
return null;
}
}

View file

@ -37,6 +37,7 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
final bitcoinCashIcon = Image.asset('assets/images/bch_icon.png', height: 24, width: 24);
final nanoIcon = Image.asset('assets/images/nano_icon.png', height: 24, width: 24);
final bananoIcon = Image.asset('assets/images/nano_icon.png', height: 24, width: 24);
final solanaIcon = Image.asset('assets/images/sol_icon.png', height: 24, width: 24);
final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24);
Image _newWalletImage(BuildContext context) => Image.asset(
@ -153,6 +154,8 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
return bananoIcon;
case WalletType.polygon:
return polygonIcon;
case WalletType.solana:
return solanaIcon;
default:
return nonWalletTypeIcon;
}

View file

@ -8,6 +8,7 @@ import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -16,12 +17,12 @@ class EditTokenPage extends BasePage {
EditTokenPage({
Key? key,
required this.homeSettingsViewModel,
this.erc20token,
this.token,
this.initialContractAddress,
}) : assert(erc20token == null || initialContractAddress == null);
}) : assert(token == null || initialContractAddress == null);
final HomeSettingsViewModel homeSettingsViewModel;
final Erc20Token? erc20token;
final CryptoCurrency? token;
final String? initialContractAddress;
@override
@ -31,7 +32,7 @@ class EditTokenPage extends BasePage {
Widget body(BuildContext context) {
return EditTokenPageBody(
homeSettingsViewModel: homeSettingsViewModel,
erc20token: erc20token,
token: token,
initialContractAddress: initialContractAddress,
);
}
@ -41,12 +42,12 @@ class EditTokenPageBody extends StatefulWidget {
const EditTokenPageBody({
Key? key,
required this.homeSettingsViewModel,
this.erc20token,
this.token,
this.initialContractAddress,
}) : super(key: key);
final HomeSettingsViewModel homeSettingsViewModel;
final Erc20Token? erc20token;
final CryptoCurrency? token;
final String? initialContractAddress;
@override
@ -73,11 +74,15 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
void initState() {
super.initState();
if (widget.erc20token != null) {
_contractAddressController.text = widget.erc20token!.contractAddress;
_tokenNameController.text = widget.erc20token!.name;
_tokenSymbolController.text = widget.erc20token!.symbol;
_tokenDecimalController.text = widget.erc20token!.decimal.toString();
String? address;
if (widget.token != null) {
address = widget.homeSettingsViewModel.getTokenAddressBasedOnWallet(widget.token!);
_contractAddressController.text = address ?? '';
_tokenNameController.text = widget.token!.name;
_tokenSymbolController.text = widget.token!.title;
_tokenDecimalController.text = widget.token!.decimals.toString();
}
if (widget.initialContractAddress != null) {
@ -91,7 +96,7 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
}
final contractAddress = _contractAddressController.text;
if (contractAddress.isNotEmpty && contractAddress != widget.erc20token?.contractAddress) {
if (contractAddress.isNotEmpty && contractAddress != address) {
setState(() {
_showDisclaimer = true;
});
@ -139,7 +144,9 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
color: Theme.of(context)
.extension<TransactionTradeTheme>()!
.detailsTitlesColor,
),
),
),
@ -172,12 +179,12 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
Expanded(
child: PrimaryButton(
onPressed: () async {
if (widget.erc20token != null) {
await widget.homeSettingsViewModel.deleteErc20Token(widget.erc20token!);
if (widget.token != null) {
await widget.homeSettingsViewModel.deleteToken(widget.token!);
}
Navigator.pop(context);
},
text: widget.erc20token != null ? S.of(context).delete : S.of(context).cancel,
text: widget.token != null ? S.of(context).delete : S.of(context).cancel,
color: Colors.red,
textColor: Colors.white,
),
@ -188,7 +195,7 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
onPressed: () async {
if (_formKey.currentState!.validate() &&
(!_showDisclaimer || _disclaimerChecked)) {
await widget.homeSettingsViewModel.addErc20Token(Erc20Token(
await widget.homeSettingsViewModel.addToken(Erc20Token(
name: _tokenNameController.text,
symbol: _tokenSymbolController.text,
contractAddress: _contractAddressController.text,
@ -214,14 +221,13 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
void _getTokenInfo() async {
if (_contractAddressController.text.isNotEmpty) {
final token =
await widget.homeSettingsViewModel.getErc20Token(_contractAddressController.text);
final token = await widget.homeSettingsViewModel.getToken(_contractAddressController.text);
if (token != null) {
if (_tokenNameController.text.isEmpty) _tokenNameController.text = token.name;
if (_tokenSymbolController.text.isEmpty) _tokenSymbolController.text = token.symbol;
if (_tokenSymbolController.text.isEmpty) _tokenSymbolController.text = token.title;
if (_tokenDecimalController.text.isEmpty)
_tokenDecimalController.text = token.decimal.toString();
_tokenDecimalController.text = token.decimals.toString();
}
}
}

View file

@ -5,6 +5,7 @@ import 'package:cake_wallet/entities/sort_balance_types.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/themes/extensions/address_theme.dart';
@ -117,7 +118,7 @@ class HomeSettingsPage extends BasePage {
return SettingsSwitcherCell(
title: "${token.name} "
"(${token.symbol})",
"(${token.title})",
value: token.enabled,
onValueChange: (_, bool value) {
_homeSettingsViewModel.changeTokenAvailability(token, value);
@ -128,20 +129,16 @@ class HomeSettingsPage extends BasePage {
'token': token,
});
},
leading: token.iconPath != null
? Container(
child: Image.asset(
token.iconPath!,
height: 30.0,
width: 30.0,
),
)
: Container(
leading: CakeImageWidget(
imageUrl: token.iconPath,
height: 40,
width: 40,
displayOnError: Container(
height: 30.0,
width: 30.0,
child: Center(
child: Text(
token.symbol.substring(0, min(token.symbol.length, 2)),
token.title.substring(0, min(token.title.length, 2)),
style: TextStyle(fontSize: 11),
),
),
@ -149,7 +146,8 @@ class HomeSettingsPage extends BasePage {
shape: BoxShape.circle,
color: Colors.grey.shade400,
),
),
),
),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(30),

View file

@ -6,6 +6,7 @@ import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart';
import 'package:cake_wallet/src/widgets/introducing_card.dart';
import 'package:cake_wallet/store/settings_store.dart';
@ -333,15 +334,11 @@ class BalanceRowWidget extends StatelessWidget {
child: Center(
child: Column(
children: [
currency.iconPath != null
? Container(
child: Image.asset(
currency.iconPath!,
height: 40.0,
width: 40.0,
),
)
: Container(
CakeImageWidget(
imageUrl: currency.iconPath,
height: 40,
width: 40,
displayOnError: Container(
height: 30.0,
width: 30.0,
child: Center(
@ -355,6 +352,7 @@ class BalanceRowWidget extends StatelessWidget {
color: Colors.grey.shade400,
),
),
),
const SizedBox(height: 10),
Text(
currency.title,

View file

@ -2,7 +2,7 @@ import 'package:cake_wallet/entities/wallet_nft_response.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/nft_image_tile_widget.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/widgets/gradient_background.dart';
import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
@ -94,7 +94,7 @@ class NFTDetailsPage extends BasePage {
.syncedBackgroundColor,
),
child: NFTImageWidget(
child: CakeImageWidget(
imageUrl: nftAsset.normalizedMetadata?.imageUrl,
),
),

View file

@ -18,23 +18,23 @@ class MenuWidget extends StatefulWidget {
class MenuWidgetState extends State<MenuWidget> {
MenuWidgetState()
: this.menuWidth = 0,
this.screenWidth = 0,
this.screenHeight = 0,
this.headerHeight = 120,
this.tileHeight = 60,
this.fromTopEdge = 50,
this.fromBottomEdge = 25,
this.moneroIcon = Image.asset('assets/images/monero_menu.png'),
this.bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png'),
this.litecoinIcon = Image.asset('assets/images/litecoin_menu.png'),
this.havenIcon = Image.asset('assets/images/haven_menu.png'),
this.ethereumIcon = Image.asset('assets/images/eth_icon.png'),
this.nanoIcon = Image.asset('assets/images/nano_icon.png'),
this.bananoIcon = Image.asset('assets/images/nano_icon.png'),
: this.menuWidth = 0,
this.screenWidth = 0,
this.screenHeight = 0,
this.headerHeight = 120,
this.tileHeight = 60,
this.fromTopEdge = 50,
this.fromBottomEdge = 25,
this.moneroIcon = Image.asset('assets/images/monero_menu.png'),
this.bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png'),
this.litecoinIcon = Image.asset('assets/images/litecoin_menu.png'),
this.havenIcon = Image.asset('assets/images/haven_menu.png'),
this.ethereumIcon = Image.asset('assets/images/eth_icon.png'),
this.nanoIcon = Image.asset('assets/images/nano_icon.png'),
this.bananoIcon = Image.asset('assets/images/nano_icon.png'),
this.bitcoinCashIcon = Image.asset('assets/images/bch_icon.png'),
this.polygonIcon = Image.asset('assets/images/matic_icon.png');
this.polygonIcon = Image.asset('assets/images/matic_icon.png'),
this.solanaIcon = Image.asset('assets/images/sol_icon.png');
final largeScreen = 731;
@ -56,7 +56,7 @@ class MenuWidgetState extends State<MenuWidget> {
Image nanoIcon;
Image bananoIcon;
Image polygonIcon;
Image solanaIcon;
@override
void initState() {
@ -224,6 +224,8 @@ class MenuWidgetState extends State<MenuWidget> {
return bananoIcon;
case WalletType.polygon:
return polygonIcon;
case WalletType.solana:
return solanaIcon;
default:
throw Exception('No icon for ${type.toString()}');
}

View file

@ -1,9 +1,8 @@
import 'package:cake_wallet/entities/wallet_nft_response.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/nft_image_tile_widget.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/themes/extensions/balance_page_theme.dart';
import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class NFTTileWidget extends StatelessWidget {
@ -38,7 +37,7 @@ class NFTTileWidget extends StatelessWidget {
),
color: Theme.of(context).extension<SyncIndicatorTheme>()!.syncedBackgroundColor,
),
child: NFTImageWidget(
child: CakeImageWidget(
imageUrl: nftAsset.normalizedMetadata?.imageUrl,
),
),

View file

@ -14,6 +14,7 @@ import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/utils/request_review_handler.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
@ -439,10 +440,17 @@ class SendPage extends BasePage {
}
if (state is TransactionCommitted) {
String alertContent;
if (sendViewModel.walletType == WalletType.solana) {
alertContent =
'${S.of(_dialogContext).send_success(sendViewModel.selectedCryptoCurrency.toString())}. ${S.of(_dialogContext).waitFewSecondForTxUpdate}';
} else {
alertContent = S.of(_dialogContext).send_success(
sendViewModel.selectedCryptoCurrency.toString());
}
return AlertWithOneAction(
alertTitle: '',
alertContent: S.of(_dialogContext).send_success(
sendViewModel.selectedCryptoCurrency.toString()),
alertContent: alertContent,
buttonText: S.of(_dialogContext).ok,
buttonAction: () {
Navigator.of(_dialogContext).pop();

View file

@ -321,7 +321,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
? sendViewModel.allAmountValidator
: sendViewModel.amountValidator,
),
if (!sendViewModel.isBatchSending)
if (!sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL)
Positioned(
top: 2,
right: 0,

View file

@ -107,7 +107,7 @@ class ConnectionSyncPage extends BasePage {
);
},
),
if (isEVMCompatibleChain(dashboardViewModel.wallet.type)) ...[
if (isWalletConnectCompatibleChain(dashboardViewModel.wallet.type)) ...[
WalletConnectTile(
onTap: () => Navigator.of(context).pushNamed(Routes.walletConnectConnectionsListing),
),

View file

@ -26,7 +26,7 @@ class OtherSettingsPage extends BasePage {
padding: EdgeInsets.only(top: 10),
child: Column(
children: [
if (!_otherSettingsViewModel.changeRepresentativeEnabled)
if (_otherSettingsViewModel.displayTransactionPriority)
SettingsPickerCell(
title: S.current.settings_fee_priority,
items: priorityForWalletType(_otherSettingsViewModel.walletType),

View file

@ -1,4 +1,5 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/receive_page_theme.dart';
import 'package:flutter/material.dart';
@ -26,12 +27,11 @@ class PairingItemWidget extends StatelessWidget {
'$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
return ListTile(
leading: CircleAvatar(
backgroundImage: (metadata.icons.isNotEmpty
? NetworkImage(metadata.icons[0])
: const AssetImage(
'assets/images/default_icon.png',
)) as ImageProvider<Object>,
leading: CakeImageWidget(
imageUrl: metadata.icons.isNotEmpty ? metadata.icons[0]: null,
displayOnError: CircleAvatar(
backgroundImage: AssetImage('assets/images/default_icon.png'),
),
),
title: Text(
metadata.name,

View file

@ -103,6 +103,7 @@ class WalletListBodyState extends State<WalletListBody> {
final bitcoinCashIcon = Image.asset('assets/images/bch_icon.png', height: 24, width: 24);
final nanoIcon = Image.asset('assets/images/nano_icon.png', height: 24, width: 24);
final polygonIcon = Image.asset('assets/images/matic_icon.png', height: 24, width: 24);
final solanaIcon = Image.asset('assets/images/sol_icon.png', height: 24, width: 24);
final scrollController = ScrollController();
final double tileHeight = 60;
Flushbar<void>? _progressBar;
@ -313,6 +314,8 @@ class WalletListBodyState extends State<WalletListBody> {
return nanoIcon;
case WalletType.polygon:
return polygonIcon;
case WalletType.solana:
return solanaIcon;
default:
return nonWalletTypeIcon;
}

View file

@ -2,25 +2,45 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class NFTImageWidget extends StatelessWidget {
const NFTImageWidget({
class CakeImageWidget extends StatelessWidget {
CakeImageWidget({
required this.imageUrl,
});
Widget? displayOnError,
this.height,
this.width,
}) : _displayOnError = displayOnError ?? Icon(Icons.error);
final String? imageUrl;
final double? height;
final double? width;
final Widget? _displayOnError;
@override
Widget build(BuildContext context) {
try {
if (imageUrl == null) return Icon(Icons.error);
if (imageUrl == null) return _displayOnError!;
if (imageUrl!.contains('assets/images')) {
return Image.asset(
imageUrl!,
height: height,
width: width,
);
}
if (imageUrl!.contains('.svg')) {
return SvgPicture.network(imageUrl!);
return SvgPicture.network(
imageUrl!,
height: height,
width: width,
);
}
return Image.network(
imageUrl!,
fit: BoxFit.cover,
height: height,
width: width,
loadingBuilder: (BuildContext _, Widget child, ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) {
return child;
@ -31,7 +51,7 @@ class NFTImageWidget extends StatelessWidget {
errorBuilder: (_, __, ___) => Icon(Icons.error),
);
} catch (_) {
return Icon(Icons.error);
return _displayOnError!;
}
}
}

View file

@ -41,7 +41,7 @@ abstract class AppStoreBase with Store {
this.wallet = wallet;
this.wallet!.setExceptionHandler(ExceptionHandler.onError);
if (isEVMCompatibleChain(wallet.type)) {
if (isWalletConnectCompatibleChain(wallet.type)) {
await getIt.get<Web3WalletService>().onDispose();
getIt.get<Web3WalletService>().create();
await getIt.get<Web3WalletService>().init();

View file

@ -838,6 +838,7 @@ abstract class SettingsStoreBase with Store {
final polygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey);
final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey);
final nanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey);
final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey);
final moneroNode = nodeSource.get(nodeId);
final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId);
final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId);
@ -847,6 +848,7 @@ abstract class SettingsStoreBase with Store {
final bitcoinCashElectrumServer = nodeSource.get(bitcoinCashElectrumServerId);
final nanoNode = nodeSource.get(nanoNodeId);
final nanoPowNode = powNodeSource.get(nanoPowNodeId);
final solanaNode = nodeSource.get(solanaNodeId);
final packageInfo = await PackageInfo.fromPlatform();
final deviceName = await _getDeviceName() ?? '';
final shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true;
@ -903,6 +905,10 @@ abstract class SettingsStoreBase with Store {
powNodes[WalletType.nano] = nanoPowNode;
}
if (solanaNode != null) {
nodes[WalletType.solana] = solanaNode;
}
final savedSyncMode = SyncMode.all.firstWhere((element) {
return element.type.index == (sharedPreferences.getInt(PreferencesKey.syncModeKey) ?? 0);
});
@ -1190,6 +1196,7 @@ abstract class SettingsStoreBase with Store {
final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey);
final polygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey);
final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey);
final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey);
final moneroNode = nodeSource.get(nodeId);
final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId);
final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId);
@ -1198,7 +1205,7 @@ abstract class SettingsStoreBase with Store {
final polygonNode = nodeSource.get(polygonNodeId);
final bitcoinCashNode = nodeSource.get(bitcoinCashElectrumServerId);
final nanoNode = nodeSource.get(nanoNodeId);
final solanaNode = nodeSource.get(solanaNodeId);
if (moneroNode != null) {
nodes[WalletType.monero] = moneroNode;
}
@ -1231,6 +1238,10 @@ abstract class SettingsStoreBase with Store {
nodes[WalletType.nano] = nanoNode;
}
if (solanaNode != null) {
nodes[WalletType.solana] = solanaNode;
}
// MIGRATED:
useTOTP2FA = await SecureKey.getBool(
@ -1358,6 +1369,9 @@ abstract class SettingsStoreBase with Store {
case WalletType.polygon:
await _sharedPreferences.setInt(PreferencesKey.currentPolygonNodeIdKey, node.key as int);
break;
case WalletType.solana:
await _sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, node.key as int);
break;
default:
break;
}

View file

@ -28,7 +28,10 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store {
final SettingsStore _settingsStore;
bool get hasSeedPhraseLengthOption =>
type == WalletType.bitcoinCash || type == WalletType.ethereum;
type == WalletType.bitcoinCash ||
type == WalletType.ethereum ||
type == WalletType.polygon ||
type == WalletType.solana;
bool get hasSeedTypeOption => type == WalletType.monero;

View file

@ -18,15 +18,15 @@ import 'package:mobx/mobx.dart';
part 'balance_view_model.g.dart';
class BalanceRecord {
const BalanceRecord({
required this.availableBalance,
required this.additionalBalance,
required this.frozenBalance,
required this.fiatAvailableBalance,
required this.fiatAdditionalBalance,
required this.fiatFrozenBalance,
required this.asset,
required this.formattedAssetTitle});
const BalanceRecord(
{required this.availableBalance,
required this.additionalBalance,
required this.frozenBalance,
required this.fiatAvailableBalance,
required this.fiatAdditionalBalance,
required this.fiatFrozenBalance,
required this.asset,
required this.formattedAssetTitle});
final String fiatAdditionalBalance;
final String fiatAvailableBalance;
final String fiatFrozenBalance;
@ -41,12 +41,10 @@ class BalanceViewModel = BalanceViewModelBase with _$BalanceViewModel;
abstract class BalanceViewModelBase with Store {
BalanceViewModelBase(
{required this.appStore,
required this.settingsStore,
required this.fiatConvertationStore})
: isReversing = false,
isShowCard = appStore.wallet!.walletInfo.isShowIntroCakePayCard,
wallet = appStore.wallet! {
{required this.appStore, required this.settingsStore, required this.fiatConvertationStore})
: isReversing = false,
isShowCard = appStore.wallet!.walletInfo.isShowIntroCakePayCard,
wallet = appStore.wallet! {
reaction((_) => appStore.wallet, _onWalletChange);
}
@ -60,8 +58,7 @@ abstract class BalanceViewModelBase with Store {
bool isReversing;
@observable
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>
wallet;
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
@computed
double get price {
@ -82,7 +79,8 @@ abstract class BalanceViewModelBase with Store {
bool get isFiatDisabled => settingsStore.fiatApiMode == FiatApiMode.disabled;
@computed
bool get isHomeScreenSettingsEnabled => isEVMCompatibleChain(wallet.type);
bool get isHomeScreenSettingsEnabled =>
isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana;
@computed
bool get hasAccounts => wallet.type == WalletType.monero;
@ -97,7 +95,7 @@ abstract class BalanceViewModelBase with Store {
String get asset {
final typeFormatted = walletTypeToString(appStore.wallet!.type);
switch(wallet.type) {
switch (wallet.type) {
case WalletType.haven:
return '$typeFormatted Assets';
default:
@ -120,13 +118,14 @@ abstract class BalanceViewModelBase with Store {
@computed
String get availableBalanceLabel {
switch(wallet.type) {
switch (wallet.type) {
case WalletType.monero:
case WalletType.haven:
case WalletType.ethereum:
case WalletType.polygon:
case WalletType.nano:
case WalletType.banano:
case WalletType.solana:
return S.current.xmr_available_balance;
default:
return S.current.confirmed;
@ -135,11 +134,12 @@ abstract class BalanceViewModelBase with Store {
@computed
String get additionalBalanceLabel {
switch(wallet.type) {
switch (wallet.type) {
case WalletType.monero:
case WalletType.haven:
case WalletType.ethereum:
case WalletType.polygon:
case WalletType.solana:
return S.current.xmr_full_balance;
case WalletType.nano:
case WalletType.banano:
@ -228,15 +228,17 @@ abstract class BalanceViewModelBase with Store {
Map<CryptoCurrency, BalanceRecord> get balances {
return wallet.balance.map((key, value) {
if (displayMode == BalanceDisplayMode.hiddenBalance) {
return MapEntry(key, BalanceRecord(
availableBalance: '---',
additionalBalance: '---',
frozenBalance: '---',
fiatAdditionalBalance: isFiatDisabled ? '' : '---',
fiatAvailableBalance: isFiatDisabled ? '' : '---',
fiatFrozenBalance: isFiatDisabled ? '' : '---',
asset: key,
formattedAssetTitle: _formatterAsset(key)));
return MapEntry(
key,
BalanceRecord(
availableBalance: '---',
additionalBalance: '---',
frozenBalance: '---',
fiatAdditionalBalance: isFiatDisabled ? '' : '---',
fiatAvailableBalance: isFiatDisabled ? '' : '---',
fiatFrozenBalance: isFiatDisabled ? '' : '---',
asset: key,
formattedAssetTitle: _formatterAsset(key)));
}
final fiatCurrency = settingsStore.fiatCurrency;
final price = fiatConvertationStore.prices[key] ?? 0;
@ -245,25 +247,23 @@ abstract class BalanceViewModelBase with Store {
// throw Exception('Price is null for: $key');
// }
final additionalFiatBalance = isFiatDisabled ? '' : (fiatCurrency.toString()
+ ' '
+ _getFiatBalance(
price: price,
cryptoAmount: value.formattedAdditionalBalance));
final additionalFiatBalance = isFiatDisabled
? ''
: (fiatCurrency.toString() +
' ' +
_getFiatBalance(price: price, cryptoAmount: value.formattedAdditionalBalance));
final availableFiatBalance = isFiatDisabled ? '' : (fiatCurrency.toString()
+ ' '
+ _getFiatBalance(
price: price,
cryptoAmount: value.formattedAvailableBalance));
final frozenFiatBalance = isFiatDisabled ? '' : (fiatCurrency.toString()
+ ' '
+ _getFiatBalance(
price: price,
cryptoAmount: getFormattedFrozenBalance(value)));
final availableFiatBalance = isFiatDisabled
? ''
: (fiatCurrency.toString() +
' ' +
_getFiatBalance(price: price, cryptoAmount: value.formattedAvailableBalance));
final frozenFiatBalance = isFiatDisabled
? ''
: (fiatCurrency.toString() +
' ' +
_getFiatBalance(price: price, cryptoAmount: getFormattedFrozenBalance(value)));
return MapEntry(
key,
@ -276,12 +276,22 @@ abstract class BalanceViewModelBase with Store {
fiatFrozenBalance: frozenFiatBalance,
asset: key,
formattedAssetTitle: _formatterAsset(key)));
});
});
}
@computed
bool get hasAdditionalBalance => !isEVMCompatibleChain(wallet.type);
bool get hasAdditionalBalance => _hasAdditionBalanceForWalletType(wallet.type);
bool _hasAdditionBalanceForWalletType(WalletType type) {
switch (type) {
case WalletType.ethereum:
case WalletType.polygon:
case WalletType.solana:
return false;
default:
return true;
}
}
@computed
List<BalanceRecord> get formattedBalances {
@ -358,9 +368,7 @@ abstract class BalanceViewModelBase with Store {
@action
void _onWalletChange(
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
TransactionInfo>?
wallet) {
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>? wallet) {
if (wallet == null) {
return;
}
@ -371,7 +379,7 @@ abstract class BalanceViewModelBase with Store {
}
@action
Future<void> disableIntroCakePayCard () async {
Future<void> disableIntroCakePayCard() async {
const cardDisplayStatus = false;
wallet.walletInfo.showIntroCakePayCard = cardDisplayStatus;
await wallet.walletInfo.save();
@ -401,6 +409,6 @@ abstract class BalanceViewModelBase with Store {
}
}
String getFormattedFrozenBalance(Balance walletBalance) => walletBalance.formattedUnAvailableBalance;
String getFormattedFrozenBalance(Balance walletBalance) =>
walletBalance.formattedUnAvailableBalance;
}

View file

@ -3,6 +3,7 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/entities/sort_balance_types.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
@ -16,14 +17,14 @@ class HomeSettingsViewModel = HomeSettingsViewModelBase with _$HomeSettingsViewM
abstract class HomeSettingsViewModelBase with Store {
HomeSettingsViewModelBase(this._settingsStore, this._balanceViewModel)
: tokens = ObservableSet<Erc20Token>() {
: tokens = ObservableSet<CryptoCurrency>() {
_updateTokensList();
}
final SettingsStore _settingsStore;
final BalanceViewModel _balanceViewModel;
final ObservableSet<Erc20Token> tokens;
final ObservableSet<CryptoCurrency> tokens;
@observable
String searchText = '';
@ -43,7 +44,7 @@ abstract class HomeSettingsViewModelBase with Store {
@action
void setPinNativeToken(bool value) => _settingsStore.pinNativeTokenAtTop = value;
Future<void> addErc20Token(Erc20Token token) async {
Future<void> addToken(CryptoCurrency token) async {
if (_balanceViewModel.wallet.type == WalletType.ethereum) {
await ethereum!.addErc20Token(_balanceViewModel.wallet, token);
}
@ -52,23 +53,31 @@ abstract class HomeSettingsViewModelBase with Store {
await polygon!.addErc20Token(_balanceViewModel.wallet, token);
}
if (_balanceViewModel.wallet.type == WalletType.solana) {
await solana!.addSPLToken(_balanceViewModel.wallet, token);
}
_updateTokensList();
_updateFiatPrices(token);
}
Future<void> deleteErc20Token(Erc20Token token) async {
Future<void> deleteToken(CryptoCurrency token) async {
if (_balanceViewModel.wallet.type == WalletType.ethereum) {
await ethereum!.deleteErc20Token(_balanceViewModel.wallet, token);
await ethereum!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token);
}
if (_balanceViewModel.wallet.type == WalletType.polygon) {
await polygon!.deleteErc20Token(_balanceViewModel.wallet, token);
await polygon!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token);
}
if (_balanceViewModel.wallet.type == WalletType.solana) {
await solana!.deleteSPLToken(_balanceViewModel.wallet, token);
}
_updateTokensList();
}
Future<Erc20Token?> getErc20Token(String contractAddress) async {
Future<CryptoCurrency?> getToken(String contractAddress) async {
if (_balanceViewModel.wallet.type == WalletType.ethereum) {
return await ethereum!.getErc20Token(_balanceViewModel.wallet, contractAddress);
}
@ -77,12 +86,16 @@ abstract class HomeSettingsViewModelBase with Store {
return await polygon!.getErc20Token(_balanceViewModel.wallet, contractAddress);
}
if (_balanceViewModel.wallet.type == WalletType.solana) {
return await solana!.getSPLToken(_balanceViewModel.wallet, contractAddress);
}
return null;
}
CryptoCurrency get nativeToken => _balanceViewModel.wallet.currency;
void _updateFiatPrices(Erc20Token token) async {
void _updateFiatPrices(CryptoCurrency token) async {
try {
_balanceViewModel.fiatConvertationStore.prices[token] =
await FiatConversionService.fetchPrice(
@ -92,20 +105,27 @@ abstract class HomeSettingsViewModelBase with Store {
} catch (_) {}
}
void changeTokenAvailability(Erc20Token token, bool value) async {
void changeTokenAvailability(CryptoCurrency token, bool value) async {
token.enabled = value;
if (_balanceViewModel.wallet.type == WalletType.ethereum) {
ethereum!.addErc20Token(_balanceViewModel.wallet, token);
ethereum!.addErc20Token(_balanceViewModel.wallet, token as Erc20Token);
}
if (_balanceViewModel.wallet.type == WalletType.polygon) {
polygon!.addErc20Token(_balanceViewModel.wallet, token);
polygon!.addErc20Token(_balanceViewModel.wallet, token as Erc20Token);
}
if (_balanceViewModel.wallet.type == WalletType.solana) {
solana!.addSPLToken(_balanceViewModel.wallet, token);
}
_refreshTokensList();
}
@action
void _updateTokensList() {
int _sortFunc(Erc20Token e1, Erc20Token e2) {
int _sortFunc(CryptoCurrency e1, CryptoCurrency e2) {
int index1 = _balanceViewModel.formattedBalances.indexWhere((element) => element.asset == e1);
int index2 = _balanceViewModel.formattedBalances.indexWhere((element) => element.asset == e2);
@ -138,6 +158,14 @@ abstract class HomeSettingsViewModelBase with Store {
.toList()
..sort(_sortFunc));
}
if (_balanceViewModel.wallet.type == WalletType.solana) {
tokens.addAll(solana!
.getSPLTokenCurrencies(_balanceViewModel.wallet)
.where((element) => _matchesSearchText(element))
.toList()
..sort(_sortFunc));
}
}
@action
@ -153,10 +181,32 @@ abstract class HomeSettingsViewModelBase with Store {
_updateTokensList();
}
bool _matchesSearchText(Erc20Token asset) {
bool _matchesSearchText(CryptoCurrency asset) {
final address = getTokenAddressBasedOnWallet(asset);
// The homes settings would only be displayed for either of Ethereum, Polygon or Solana Wallets.
if (address == null) return false;
return searchText.isEmpty ||
asset.fullName!.toLowerCase().contains(searchText.toLowerCase()) ||
asset.title.toLowerCase().contains(searchText.toLowerCase()) ||
asset.contractAddress == searchText;
address == searchText;
}
String? getTokenAddressBasedOnWallet(CryptoCurrency asset) {
if (_balanceViewModel.wallet.type == WalletType.solana) {
return solana!.getTokenAddress(asset);
}
if (_balanceViewModel.wallet.type == WalletType.ethereum) {
return ethereum!.getTokenAddress(asset);
}
if (_balanceViewModel.wallet.type == WalletType.polygon) {
return polygon!.getTokenAddress(asset);
}
// 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).
return null;
}
}

View file

@ -4,6 +4,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/nano/nano.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cake_wallet/store/settings_store.dart';
@ -105,6 +106,14 @@ class TransactionListItem extends ActionListItem with Keyable {
nano!.getTransactionAmountRaw(transaction).toString(), nanoUtil!.rawPerNano)),
price: price);
break;
case WalletType.solana:
final asset = solana!.assetOfTransaction(balanceViewModel.wallet, transaction);
final price = balanceViewModel.fiatConvertationStore.prices[asset];
amount = calculateFiatAmountRaw(
cryptoAmount: solana!.getTransactionAmountRaw(transaction),
price: price,
);
break;
default:
break;
}

View file

@ -625,6 +625,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
depositCurrency = CryptoCurrency.maticpoly;
receiveCurrency = CryptoCurrency.xmr;
break;
case WalletType.solana:
depositCurrency = CryptoCurrency.sol;
receiveCurrency = CryptoCurrency.xmr;
break;
default:
break;
}

View file

@ -75,6 +75,9 @@ abstract class NodeListViewModelBase with Store {
case WalletType.polygon:
node = getPolygonDefaultNode(nodes: _nodeSource)!;
break;
case WalletType.solana:
node = getSolanaDefaultNode(nodes: _nodeSource)!;
break;
default:
throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}');
}

View file

@ -3,6 +3,7 @@ import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/nano/nano.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/view_model/restore/restore_mode.dart';
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
import 'package:hive/hive.dart';
@ -75,6 +76,9 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store
case WalletType.polygon:
return polygon!.createPolygonRestoreWalletFromPrivateKey(
name: name, password: password, privateKey: restoreWallet.privateKey!);
case WalletType.solana:
return solana!.createSolanaRestoreWalletFromPrivateKey(
name: name, password: password, privateKey: restoreWallet.privateKey!);
default:
throw Exception('Unexpected type: ${restoreWallet.type.toString()}');
}
@ -102,6 +106,9 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store
case WalletType.polygon:
return polygon!.createPolygonRestoreWalletFromSeedCredentials(
name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password);
case WalletType.solana:
return solana!.createSolanaRestoreWalletFromSeedCredentials(
name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password);
default:
throw Exception('Unexpected type: ${type.toString()}');
}

View file

@ -32,6 +32,7 @@ class WalletRestoreFromQRCode {
'bitcoincash': WalletType.bitcoinCash,
'bitcoincash-wallet': WalletType.bitcoinCash,
'bitcoincash_wallet': WalletType.bitcoinCash,
'solana-wallet': WalletType.solana,
};
static bool _containsAssetSpecifier(String code) => _extractWalletType(code) != null;
@ -175,6 +176,14 @@ class WalletRestoreFromQRCode {
return WalletRestoreMode.seed;
}
if (type == WalletType.solana && credentials.containsKey('private_key')) {
final privateKey = credentials['private_key'] as String;
if (privateKey.isEmpty) {
throw Exception('Unexpected restore mode: private_key');
}
return WalletRestoreMode.keys;
}
throw Exception('Unexpected restore mode: restore params are invalid');
}
}

View file

@ -148,9 +148,8 @@ abstract class OutputBase with Store {
@computed
String get estimatedFeeFiatAmount {
try {
final currency = isEVMCompatibleChain(_wallet.type)
? _wallet.currency
: cryptoCurrencyHandler();
final currency =
isEVMCompatibleChain(_wallet.type) ? _wallet.currency : cryptoCurrencyHandler();
final fiat = calculateFiatAmountRaw(
price: _fiatConversationStore.prices[currency]!, cryptoAmount: estimatedFee);
return fiat;
@ -220,7 +219,6 @@ abstract class OutputBase with Store {
final crypto = double.parse(fiatAmount.replaceAll(',', '.')) /
_fiatConversationStore.prices[cryptoCurrencyHandler()]!;
final cryptoAmountTmp = _cryptoNumberFormat.format(crypto);
if (cryptoAmount != cryptoAmountTmp) {
cryptoAmount = cryptoAmountTmp;
}
@ -252,6 +250,9 @@ abstract class OutputBase with Store {
case WalletType.polygon:
maximumFractionDigits = 12;
break;
case WalletType.solana:
maximumFractionDigits = 12;
break;
default:
break;
}

View file

@ -52,7 +52,8 @@ abstract class SendTemplateViewModelBase with Store {
bool get hasMultiRecipient =>
_wallet.type != WalletType.haven &&
_wallet.type != WalletType.ethereum &&
_wallet.type != WalletType.polygon;
_wallet.type != WalletType.polygon &&
_wallet.type != WalletType.solana;
@computed
CryptoCurrency get cryptoCurrency => _wallet.currency;

View file

@ -7,6 +7,7 @@ import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
@ -44,7 +45,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
void onWalletChange(wallet) {
currencies = wallet.balance.keys.toList();
selectedCryptoCurrency = wallet.currency;
hasMultipleTokens = isEVMCompatibleChain(wallet.type);
hasMultipleTokens = isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana;
}
SendViewModelBase(
@ -57,7 +58,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
) : state = InitialExecutionState(),
currencies = appStore.wallet!.balance.keys.toList(),
selectedCryptoCurrency = appStore.wallet!.currency,
hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type),
hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type) ||
appStore.wallet!.type == WalletType.solana,
outputs = ObservableList<Output>(),
_settingsStore = appStore.settingsStore,
fiatFromSettings = appStore.settingsStore.fiatCurrency,
@ -100,6 +102,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
@computed
bool get isBatchSending => outputs.length > 1;
bool get shouldDisplaySendALL => walletType != WalletType.solana;
@computed
String get pendingTransactionFiatAmount {
if (pendingTransaction == null) {
@ -297,6 +301,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
pendingTransaction = await wallet.createTransaction(_credentials());
state = ExecutedSuccessfullyState();
} catch (e) {
print('Failed with ${e.toString()}');
state = FailureState(e.toString());
}
}
@ -351,7 +356,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
Object _credentials() {
final priority = _settingsStore.priority[wallet.type];
if (priority == null && wallet.type != WalletType.nano) {
if (priority == null && wallet.type != WalletType.nano && wallet.type != WalletType.solana) {
throw Exception('Priority is null for wallet type: ${wallet.type}');
}
@ -377,6 +382,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
case WalletType.polygon:
return polygon!.createPolygonTransactionCredentials(outputs,
priority: priority!, currency: selectedCryptoCurrency);
case WalletType.solana:
return solana!
.createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency);
default:
throw Exception('Unexpected wallet type: ${wallet.type}');
}

View file

@ -14,15 +14,14 @@ import 'package:package_info/package_info.dart';
part 'other_settings_view_model.g.dart';
class OtherSettingsViewModel = OtherSettingsViewModelBase
with _$OtherSettingsViewModel;
class OtherSettingsViewModel = OtherSettingsViewModelBase with _$OtherSettingsViewModel;
abstract class OtherSettingsViewModelBase with Store {
OtherSettingsViewModelBase(this._settingsStore, this._wallet)
: walletType = _wallet.type,
currentVersion = '' {
PackageInfo.fromPlatform().then(
(PackageInfo packageInfo) => currentVersion = packageInfo.version);
PackageInfo.fromPlatform()
.then((PackageInfo packageInfo) => currentVersion = packageInfo.version);
final priority = _settingsStore.priority[_wallet.type];
final priorities = priorityForWalletType(_wallet.type);
@ -33,8 +32,7 @@ abstract class OtherSettingsViewModelBase with Store {
}
final WalletType walletType;
final WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
TransactionInfo> _wallet;
final WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> _wallet;
@observable
String currentVersion;
@ -57,12 +55,14 @@ abstract class OtherSettingsViewModelBase with Store {
_wallet.type == WalletType.nano || _wallet.type == WalletType.banano;
@computed
bool get isEnabledBuyAction =>
!_settingsStore.disableBuy && _wallet.type != WalletType.haven;
bool get displayTransactionPriority =>
!(changeRepresentativeEnabled || _wallet.type == WalletType.solana);
@computed
bool get isEnabledSellAction =>
!_settingsStore.disableSell && _wallet.type != WalletType.haven;
bool get isEnabledBuyAction => !_settingsStore.disableBuy && _wallet.type != WalletType.haven;
@computed
bool get isEnabledSellAction => !_settingsStore.disableSell && _wallet.type != WalletType.haven;
List<ProviderType> get availableBuyProvidersTypes {
return ProvidersHelper.getAvailableBuyProviderTypes(walletType);
@ -72,12 +72,10 @@ abstract class OtherSettingsViewModelBase with Store {
ProvidersHelper.getAvailableSellProviderTypes(walletType);
ProviderType get buyProviderType =>
_settingsStore.defaultBuyProviders[walletType] ??
ProviderType.askEachTime;
_settingsStore.defaultBuyProviders[walletType] ?? ProviderType.askEachTime;
ProviderType get sellProviderType =>
_settingsStore.defaultSellProviders[walletType] ??
ProviderType.askEachTime;
_settingsStore.defaultSellProviders[walletType] ?? ProviderType.askEachTime;
String getDisplayPriority(dynamic priority) {
final _priority = priority as TransactionPriority;
@ -114,7 +112,6 @@ abstract class OtherSettingsViewModelBase with Store {
_settingsStore.defaultBuyProviders[walletType] = buyProviderType;
@action
ProviderType onSellProviderTypeSelected(
ProviderType sellProviderType) =>
ProviderType onSellProviderTypeSelected(ProviderType sellProviderType) =>
_settingsStore.defaultSellProviders[walletType] = sellProviderType;
}

View file

@ -54,6 +54,9 @@ abstract class TransactionDetailsViewModelBase with Store {
case WalletType.polygon:
_addPolygonListItems(tx, dateFormat);
break;
case WalletType.solana:
_addSolanaListItems(tx, dateFormat);
break;
default:
break;
}
@ -131,6 +134,8 @@ abstract class TransactionDetailsViewModelBase with Store {
return 'https://bananolooker.com/block/${txId}';
case WalletType.polygon:
return 'https://polygonscan.com/tx/${txId}';
case WalletType.solana:
return 'https://solscan.io/tx/${txId}';
default:
return '';
}
@ -155,6 +160,8 @@ abstract class TransactionDetailsViewModelBase with Store {
return S.current.view_transaction_on + 'bananolooker.com';
case WalletType.polygon:
return S.current.view_transaction_on + 'polygonscan.com';
case WalletType.solana:
return S.current.view_transaction_on + 'solscan.io';
default:
return '';
}
@ -281,4 +288,21 @@ abstract class TransactionDetailsViewModelBase with Store {
items.addAll(_items);
}
void _addSolanaListItems(TransactionInfo tx, DateFormat dateFormat) {
final _items = [
StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id),
StandartListItem(
title: S.current.transaction_details_date, value: dateFormat.format(tx.date)),
StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()),
if (tx.feeFormatted()?.isNotEmpty ?? false)
StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!),
if (showRecipientAddress && tx.to != null)
StandartListItem(title: S.current.transaction_details_recipient_address, value: tx.to!),
if (tx.from != null)
StandartListItem(title: S.current.transaction_details_source_address, value: tx.from!),
];
items.addAll(_items);
}
}

View file

@ -7,6 +7,7 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/haven/haven.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
@ -159,6 +160,21 @@ class PolygonURI extends PaymentURI {
}
}
class SolanaURI extends PaymentURI {
SolanaURI({required String amount, required String address})
: super(amount: amount, address: address);
@override
String toString() {
var base = 'solana:' + address;
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store {
WalletAddressListViewModelBase({
required AppStore appStore,
@ -257,6 +273,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
return PolygonURI(amount: amount, address: address.address);
}
if (wallet.type == WalletType.solana) {
return SolanaURI(amount: amount, address: address.address);
}
throw Exception('Unexpected type: ${type.toString()}');
}
@ -326,6 +346,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress));
}
if (wallet.type == WalletType.solana) {
final primaryAddress = solana!.getAddress(wallet);
addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress));
}
if (searchText.isNotEmpty) {
return ObservableList.of(addressList.where((item) {
if (item is WalletAddressListItem) {

View file

@ -110,7 +110,8 @@ abstract class WalletKeysViewModelBase with Store {
]);
}
if (isEVMCompatibleChain(_appStore.wallet!.type)) {
if (isEVMCompatibleChain(_appStore.wallet!.type) ||
_appStore.wallet!.type == WalletType.solana) {
items.addAll([
if (_appStore.wallet!.privateKey != null)
StandartListItem(title: S.current.private_key, value: _appStore.wallet!.privateKey!),
@ -165,6 +166,8 @@ abstract class WalletKeysViewModelBase with Store {
return 'banano-wallet';
case WalletType.polygon:
return 'polygon-wallet';
case WalletType.solana:
return 'solana-wallet';
default:
throw Exception('Unexpected wallet type: ${_appStore.wallet!.toString()}');
}

View file

@ -1,5 +1,6 @@
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/monero/monero.dart';
@ -36,19 +37,21 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
bool get hasLanguageSelector => type == WalletType.monero || type == WalletType.haven;
int get seedPhraseWordsLength {
switch (type) {
case WalletType.monero:
if(advancedPrivacySettingsViewModel.isPolySeed) {
return 16;
}
return 25;
case WalletType.ethereum:
case WalletType.bitcoinCash:
return advancedPrivacySettingsViewModel.seedPhraseLength.value;
default:
return 24;
}
switch (type) {
case WalletType.monero:
if (advancedPrivacySettingsViewModel.isPolySeed) {
return 16;
}
return 25;
case WalletType.solana:
case WalletType.polygon:
case WalletType.ethereum:
case WalletType.bitcoinCash:
return advancedPrivacySettingsViewModel.seedPhraseLength.value;
default:
return 24;
}
}
bool get hasSeedType => type == WalletType.monero;
@ -64,8 +67,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
case WalletType.litecoin:
return bitcoin!.createBitcoinNewWalletCredentials(name: name);
case WalletType.haven:
return haven!.createHavenNewWalletCredentials(
name: name, language: options!.first as String);
return haven!
.createHavenNewWalletCredentials(name: name, language: options!.first as String);
case WalletType.ethereum:
return ethereum!.createEthereumNewWalletCredentials(name: name);
case WalletType.bitcoinCash:
@ -74,6 +77,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
return nano!.createNanoNewWalletCredentials(name: name);
case WalletType.polygon:
return polygon!.createPolygonNewWalletCredentials(name: name);
case WalletType.solana:
return solana!.createSolanaNewWalletCredentials(name: name);
default:
throw Exception('Unexpected type: ${type.toString()}');
}

View file

@ -4,6 +4,7 @@ import 'package:cake_wallet/nano/nano.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/store/app_store.dart';
@ -28,11 +29,11 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
{required WalletType type})
: hasSeedLanguageSelector = type == WalletType.monero || type == WalletType.haven,
hasBlockchainHeightLanguageSelector = type == WalletType.monero || type == WalletType.haven,
hasRestoreFromPrivateKey =
type == WalletType.ethereum ||
hasRestoreFromPrivateKey = type == WalletType.ethereum ||
type == WalletType.polygon ||
type == WalletType.nano ||
type == WalletType.banano,
type == WalletType.banano ||
type == WalletType.solana,
isButtonEnabled = false,
mode = WalletRestoreMode.seed,
super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true) {
@ -45,6 +46,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
break;
case WalletType.nano:
case WalletType.banano:
case WalletType.solana:
availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys];
break;
default:
@ -98,22 +100,21 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
name: name, height: height, mnemonic: seed, password: password);
case WalletType.ethereum:
return ethereum!.createEthereumRestoreWalletFromSeedCredentials(
name: name,
mnemonic: seed,
password: password);
name: name, mnemonic: seed, password: password);
case WalletType.bitcoinCash:
return bitcoinCash!.createBitcoinCashRestoreWalletFromSeedCredentials(
name: name,
mnemonic: seed,
password: password);
name: name, mnemonic: seed, password: password);
case WalletType.nano:
return nano!.createNanoRestoreWalletFromSeedCredentials(
name: name, mnemonic: seed, password: password, derivationType: derivationType);
case WalletType.polygon:
return polygon!.createPolygonRestoreWalletFromSeedCredentials(
name: name,
mnemonic: seed,
password: password,
derivationType: derivationType);
case WalletType.polygon:
return polygon!.createPolygonRestoreWalletFromSeedCredentials(
);
case WalletType.solana:
return solana!.createSolanaRestoreWalletFromSeedCredentials(
name: name,
mnemonic: seed,
password: password,
@ -160,16 +161,22 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
case WalletType.nano:
return nano!.createNanoRestoreWalletFromKeysCredentials(
name: name,
password: password,
seedKey: options['private_key'] as String,
derivationType: options["derivationType"] as DerivationType);
name: name,
password: password,
seedKey: options['private_key'] as String,
derivationType: options["derivationType"] as DerivationType);
case WalletType.polygon:
return polygon!.createPolygonRestoreWalletFromPrivateKey(
name: name,
password: password,
privateKey: options['private_key'] as String,
);
case WalletType.solana:
return solana!.createSolanaRestoreWalletFromPrivateKey(
name: name,
password: password,
privateKey: options['private_key'] as String,
);
default:
break;
}
@ -187,10 +194,8 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
switch (type) {
case WalletType.nano:
return nanoUtil!.compareDerivationMethods(
mnemonic: mnemonic,
privateKey: seedKey,
node: node);
return nanoUtil!
.compareDerivationMethods(mnemonic: mnemonic, privateKey: seedKey, node: node);
default:
break;
}

View file

@ -5,6 +5,7 @@ cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build
cd cw_haven && 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_bitcoin_cash && 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

View file

@ -106,6 +106,7 @@ dependencies:
flutter_svg: ^2.0.9
polyseed: ^0.0.2
nostr_tools: ^1.0.9
solana: ^0.30.1
dev_dependencies:
flutter_test:
@ -152,6 +153,7 @@ flutter:
- assets/nano_node_list.yml
- assets/nano_pow_node_list.yml
- assets/polygon_node_list.yml
- assets/solana_node_list.yml
- assets/text/
- assets/faq/
- assets/animation/

Some files were not shown because too many files have changed in this diff Show more