mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-11-16 17:27:37 +00:00
Cw 78 ethereum (#862)
* Add initial flow for ethereum * Add initial create Eth wallet flow * Complete Ethereum wallet creation flow * Fix web3dart versioning issue * Add primary receive address extracted from private key * Implement open wallet functionality * Implement restore wallet from seed functionality * Fixate web3dart version as higher versions cause some issues * Add Initial Transaction priorities for eth Add estimated gas price * Rename priority value to tip * Re-order wallet types * Change ethereum node Fix connection issues * Fix estimating gas for priority * Add case for ethereum to fetch it's seeds * Add case for ethereum to request node * Fix Exchange screen initial pairs * Add initial send transaction flow * Add missing configure for ethereum class * Add Eth address initial setup * Fix Private key for Ethereum wallets * Change sign/send transaction flow * - Fix Conflicts with main - Remove unused function from Haven configure.dart * Add build command for ethereum package * Add missing Node list file to pubspec * - Fix balance display - Fix parsing of Ethereum amount - Add more Ethereum Nodes * - Fix extracting Ethereum Private key from seeds - Integrate signing/sending transaction with the send view model * - Update and Fix Conflicts with main * Add Balances for ERC20 tokens * Fix conflicts with main * Add erc20 abi json * Add send erc20 tokens initial function * add missing getHeightByDate in Haven * Allow contacts and wallets from the same tag * Add Shiba Inu icon * Add send ERC-20 tokens initial flow * Add missing import in generated file * Add initial approach for transaction sending for ERC-20 tokens * Refactor signing/sending transactions * Add initial flow for transactions subscription * Refactor signing/sending transactions * Add home settings icon * Fix conflicts with main * Initial flow for home settings * Add logic flow for adding erc20 tokens * Fix initial UI * Finalize UI for Tokens * Integrate UI with Ethereum flow * Add "Enable/Disable" feature for ERC20 tokens * Add initial Erc20 tokens * Add Sorting and Pin Native Token features * Fix price sorting * Sort tokens list as well when Sort criteria changes * - Improve sorting balances flow - Add initial add token from search bar flow * Fix Accounts Popup UI * Fix Pin native token * Fix Enabling/Disabling tokens Fix sorting by fiat once app is opened Improve token availability mechanism * Fix deleting token Fix renaming tokens * Fix issue with search * Add more tokens * - Fix scroll issue - Add ERC20 tokens placeholder image in picker * - Separate and organize default erc20 tokens - Fix scrolling - Add token placeholder images in picker - Sort disabled tokens alphabetically * Change BNB token initial availability * Fix Conflicts with main * Fix Conflicts with main * Add Verse ERC20 token to the initial tokens list * Add rename wallet to Ethereum * Integrate EtherScan API for fetching address transactions Generate Ethereum specific secrets in Ethereum package * Adjust transactions fiat price for ERC20 tokens * Free Up GitHub Actions Ubuntu Runner Disk Space * Free Up GitHub Actions Ubuntu Runner Disk space (trial 2) * Fix Transaction Fee display * Save transaction history * Enhance loading time for erc20 tokens transactions * Minor Fixes and Enhancements * Fix sending erc20 fix block explorer issue * Fix int overflow * Fix transaction amount conversions * Minor: `slow` -> `Slow` * Update build guide * Fix fetching fiat rate taking a lot of time by only fetching enabled tokens only and making the API calls in parallel not sequential * Update transactions on a periodic basis * For fee, use ETH spot price, not ERC-20 spot price * Add Etherscan History privacy option to enable/disable Etherscan API * Show estimated fee amounts in the send screen * fix send fiat fields parsing issue * Fix transactions estimated fee less than actual fee * handle balance sorting when balance is disabled Handle empty transactions list * Fix Delete Ethereum wallet Fix balance < 0.01 * Fix Decimal place for Ethereum amount Fix sending amount issue * Change words count * Remove balance hint and Full balance row from Ethereum wallets * support changing the asset type in send templates * Fix Templates for ERC tokens issues * Fix conflicts in send templates * Disable batch sending in Ethereum * Fix Fee calculation with different priorities * Fix Conflicts with main * Add offline error to ignored exceptions --------- Co-authored-by: Justin Ehrenhofer <justin.ehrenhofer@gmail.com>
This commit is contained in:
parent
4120394121
commit
3ce4000dcf
137 changed files with 7164 additions and 1492 deletions
2
.github/workflows/pr_test_build.yml
vendored
2
.github/workflows/pr_test_build.yml
vendored
|
@ -92,6 +92,7 @@ jobs:
|
|||
cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
flutter packages pub run build_runner build --delete-conflicting-outputs
|
||||
|
||||
- name: Add secrets
|
||||
|
@ -124,6 +125,7 @@ jobs:
|
|||
echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart
|
||||
echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart
|
||||
echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart
|
||||
echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_ethereum/lib/.secrets.g.dart
|
||||
|
||||
- name: Rename app
|
||||
run: echo -e "id=com.cakewallet.test\nname=$GITHUB_HEAD_REF" > /opt/android/cake_wallet/android/app.properties
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -90,7 +90,9 @@ android/key.properties
|
|||
**/tool/.secrets-prod.json
|
||||
**/tool/.secrets-test.json
|
||||
**/tool/.secrets-config.json
|
||||
**/tool/.ethereum-secrets-config.json
|
||||
**/lib/.secrets.g.dart
|
||||
**/cw_ethereum/lib/.secrets.g.dart
|
||||
|
||||
vendor/
|
||||
|
||||
|
@ -121,6 +123,7 @@ cw_haven/android/.cxx/
|
|||
lib/bitcoin/bitcoin.dart
|
||||
lib/monero/monero.dart
|
||||
lib/haven/haven.dart
|
||||
lib/ethereum/ethereum.dart
|
||||
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png
|
||||
ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png
|
||||
|
|
10
assets/ethereum_server_list.yml
Normal file
10
assets/ethereum_server_list.yml
Normal file
|
@ -0,0 +1,10 @@
|
|||
-
|
||||
uri: ethereum.publicnode.com
|
||||
-
|
||||
uri: eth.llamarpc.com
|
||||
-
|
||||
uri: rpc.flashbots.net
|
||||
-
|
||||
uri: eth-mainnet.public.blastapi.io
|
||||
-
|
||||
uri: ethereum.publicnode.com
|
BIN
assets/images/home_screen_settings_icon.png
Normal file
BIN
assets/images/home_screen_settings_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 394 B |
10
configure_cake_wallet_android.sh
Normal file
10
configure_cake_wallet_android.sh
Normal file
|
@ -0,0 +1,10 @@
|
|||
cd scripts/android
|
||||
source ./app_env.sh cakewallet
|
||||
./app_config.sh
|
||||
cd ../.. && flutter pub get
|
||||
cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
|
||||
flutter packages pub run build_runner build --delete-conflicting-outputs
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:cw_core/transaction_history.dart';
|
||||
import 'package:cw_bitcoin/file.dart';
|
||||
|
@ -67,7 +66,7 @@ abstract class ElectrumTransactionHistoryBase
|
|||
Future<void> _load() async {
|
||||
try {
|
||||
final content = await _read();
|
||||
final txs = content['transactions'] as Map<String, dynamic> ?? {};
|
||||
final txs = content['transactions'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
txs.entries.forEach((entry) {
|
||||
final val = entry.value;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData;
|
||||
import 'package:cw_bitcoin/address_from_output.dart';
|
||||
|
@ -217,9 +216,9 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
|||
height: info.height,
|
||||
amount: info.amount,
|
||||
fee: info.fee,
|
||||
direction: direction ?? info.direction,
|
||||
date: date ?? info.date,
|
||||
isPending: isPending ?? info.isPending,
|
||||
direction: direction,
|
||||
date: date,
|
||||
isPending: isPending,
|
||||
confirmations: info.confirmations);
|
||||
}
|
||||
|
||||
|
|
|
@ -431,6 +431,7 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
|
|||
await transactionHistory.save();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> renameWalletFiles(String newWalletName) async {
|
||||
final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
|
||||
final currentWalletFile = File(currentWalletPath);
|
||||
|
|
|
@ -11,6 +11,8 @@ CryptoCurrency currencyForWalletType(WalletType type) {
|
|||
return CryptoCurrency.ltc;
|
||||
case WalletType.haven:
|
||||
return CryptoCurrency.xhv;
|
||||
case WalletType.ethereum:
|
||||
return CryptoCurrency.eth;
|
||||
default:
|
||||
throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType');
|
||||
}
|
||||
|
|
64
cw_core/lib/erc20_token.dart
Normal file
64
cw_core/lib/erc20_token.dart
Normal file
|
@ -0,0 +1,64 @@
|
|||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'erc20_token.g.dart';
|
||||
|
||||
@HiveType(typeId: Erc20Token.typeId)
|
||||
class Erc20Token extends CryptoCurrency with HiveObjectMixin {
|
||||
@HiveField(0)
|
||||
final String name;
|
||||
@HiveField(1)
|
||||
final String symbol;
|
||||
@HiveField(2)
|
||||
final String contractAddress;
|
||||
@HiveField(3)
|
||||
final int decimal;
|
||||
@HiveField(4, defaultValue: true)
|
||||
bool _enabled;
|
||||
@HiveField(5)
|
||||
final String? iconPath;
|
||||
|
||||
bool get enabled => _enabled;
|
||||
|
||||
set enabled(bool value) => _enabled = value;
|
||||
|
||||
Erc20Token({
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
required this.contractAddress,
|
||||
required this.decimal,
|
||||
bool enabled = true,
|
||||
this.iconPath,
|
||||
}) : _enabled = enabled,
|
||||
super(
|
||||
name: symbol.toLowerCase(),
|
||||
title: symbol.toUpperCase(),
|
||||
fullName: name,
|
||||
tag: "ETH",
|
||||
iconPath: iconPath,
|
||||
);
|
||||
|
||||
Erc20Token.copyWith(Erc20Token other, String? icon)
|
||||
: this.name = other.name,
|
||||
this.symbol = other.symbol,
|
||||
this.contractAddress = other.contractAddress,
|
||||
this.decimal = other.decimal,
|
||||
this._enabled = other.enabled,
|
||||
this.iconPath = icon,
|
||||
super(
|
||||
name: other.name,
|
||||
title: other.symbol.toUpperCase(),
|
||||
fullName: other.name,
|
||||
tag: "ETH",
|
||||
iconPath: icon,
|
||||
);
|
||||
|
||||
static const typeId = 12;
|
||||
static const boxName = 'Erc20Tokens';
|
||||
|
||||
@override
|
||||
bool operator ==(other) => other is Erc20Token && other.contractAddress == contractAddress;
|
||||
|
||||
@override
|
||||
int get hashCode => contractAddress.hashCode;
|
||||
}
|
|
@ -75,6 +75,8 @@ class Node extends HiveObject with Keyable {
|
|||
return createUriFromElectrumAddress(uriRaw);
|
||||
case WalletType.haven:
|
||||
return Uri.http(uriRaw, '');
|
||||
case WalletType.ethereum:
|
||||
return Uri.https(uriRaw, '');
|
||||
default:
|
||||
throw Exception('Unexpected type ${type.toString()} for Node uri');
|
||||
}
|
||||
|
@ -124,6 +126,8 @@ class Node extends HiveObject with Keyable {
|
|||
return requestElectrumServer();
|
||||
case WalletType.haven:
|
||||
return requestMoneroNode();
|
||||
case WalletType.ethereum:
|
||||
return requestElectrumServer();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
@ -193,4 +197,17 @@ class Node extends HiveObject with Keyable {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> requestEthereumServer() async {
|
||||
try {
|
||||
final response = await http.get(
|
||||
uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
|
||||
return response.statusCode >= 200 && response.statusCode < 300;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,4 +75,6 @@ abstract class WalletBase<
|
|||
Future<void>? updateBalance();
|
||||
|
||||
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => null;
|
||||
|
||||
Future<void> renameWalletFiles(String newWalletName);
|
||||
}
|
||||
|
|
|
@ -18,5 +18,5 @@ abstract class WalletService<N extends WalletCredentials,
|
|||
|
||||
Future<void> remove(String wallet);
|
||||
|
||||
Future<void> rename(String name, String password, String newName);
|
||||
Future<void> rename(String currentName, String password, String newName);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@ const walletTypes = [
|
|||
WalletType.monero,
|
||||
WalletType.bitcoin,
|
||||
WalletType.litecoin,
|
||||
WalletType.haven
|
||||
WalletType.haven,
|
||||
WalletType.ethereum,
|
||||
];
|
||||
const walletTypeTypeId = 5;
|
||||
|
||||
|
@ -27,6 +28,9 @@ enum WalletType {
|
|||
|
||||
@HiveField(4)
|
||||
haven,
|
||||
|
||||
@HiveField(5)
|
||||
ethereum,
|
||||
}
|
||||
|
||||
int serializeToInt(WalletType type) {
|
||||
|
@ -39,6 +43,8 @@ int serializeToInt(WalletType type) {
|
|||
return 2;
|
||||
case WalletType.haven:
|
||||
return 3;
|
||||
case WalletType.ethereum:
|
||||
return 4;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
|
@ -54,6 +60,8 @@ WalletType deserializeFromInt(int raw) {
|
|||
return WalletType.litecoin;
|
||||
case 3:
|
||||
return WalletType.haven;
|
||||
case 4:
|
||||
return WalletType.ethereum;
|
||||
default:
|
||||
throw Exception('Unexpected token: $raw for WalletType deserializeFromInt');
|
||||
}
|
||||
|
@ -69,6 +77,8 @@ String walletTypeToString(WalletType type) {
|
|||
return 'Litecoin';
|
||||
case WalletType.haven:
|
||||
return 'Haven';
|
||||
case WalletType.ethereum:
|
||||
return 'Ethereum';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
@ -84,6 +94,8 @@ String walletTypeToDisplayName(WalletType type) {
|
|||
return 'Litecoin (LTC)';
|
||||
case WalletType.haven:
|
||||
return 'Haven (XHV)';
|
||||
case WalletType.ethereum:
|
||||
return 'Ethereum (ETH)';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
@ -99,6 +111,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) {
|
|||
return CryptoCurrency.ltc;
|
||||
case WalletType.haven:
|
||||
return CryptoCurrency.xhv;
|
||||
case WalletType.ethereum:
|
||||
return CryptoCurrency.eth;
|
||||
default:
|
||||
throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency');
|
||||
}
|
||||
|
|
30
cw_ethereum/.gitignore
vendored
Normal file
30
cw_ethereum/.gitignore
vendored
Normal 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_ethereum/.metadata
Normal file
10
cw_ethereum/.metadata
Normal 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: eb6d86ee27deecba4a83536aa20f366a6044895c
|
||||
channel: stable
|
||||
|
||||
project_type: package
|
3
cw_ethereum/CHANGELOG.md
Normal file
3
cw_ethereum/CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
## 0.0.1
|
||||
|
||||
* TODO: Describe initial release.
|
1
cw_ethereum/LICENSE
Normal file
1
cw_ethereum/LICENSE
Normal file
|
@ -0,0 +1 @@
|
|||
TODO: Add your license here.
|
39
cw_ethereum/README.md
Normal file
39
cw_ethereum/README.md
Normal 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.
|
4
cw_ethereum/analysis_options.yaml
Normal file
4
cw_ethereum/analysis_options.yaml
Normal 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
|
7
cw_ethereum/lib/cw_ethereum.dart
Normal file
7
cw_ethereum/lib/cw_ethereum.dart
Normal file
|
@ -0,0 +1,7 @@
|
|||
library cw_ethereum;
|
||||
|
||||
/// A Calculator.
|
||||
class Calculator {
|
||||
/// Returns [value] plus 1.
|
||||
int addOne(int value) => value + 1;
|
||||
}
|
302
cw_ethereum/lib/default_erc20_tokens.dart
Normal file
302
cw_ethereum/lib/default_erc20_tokens.dart
Normal file
|
@ -0,0 +1,302 @@
|
|||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/erc20_token.dart';
|
||||
|
||||
class DefaultErc20Tokens {
|
||||
final List<Erc20Token> _defaultTokens = [
|
||||
Erc20Token(
|
||||
name: "USD Coin",
|
||||
symbol: "USDC",
|
||||
contractAddress: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
|
||||
decimal: 6,
|
||||
enabled: true,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "USDT Tether",
|
||||
symbol: "USDT",
|
||||
contractAddress: "0xdac17f958d2ee523a2206206994597c13d831ec7",
|
||||
decimal: 6,
|
||||
enabled: true,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Dai",
|
||||
symbol: "DAI",
|
||||
contractAddress: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
decimal: 18,
|
||||
enabled: true,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Wrapped Ether",
|
||||
symbol: "WETH",
|
||||
contractAddress: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Pepe",
|
||||
symbol: "PEPE",
|
||||
contractAddress: "0x6982508145454ce325ddbe47a25d4ec3d2311933",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "SHIBA INU",
|
||||
symbol: "SHIB",
|
||||
contractAddress: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "ApeCoin",
|
||||
symbol: "APE",
|
||||
contractAddress: "0x4d224452801aced8b2f0aebe155379bb5d594381",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Matic Token",
|
||||
symbol: "MATIC",
|
||||
contractAddress: "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Wrapped BTC",
|
||||
symbol: "WBTC",
|
||||
contractAddress: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599",
|
||||
decimal: 8,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Gitcoin",
|
||||
symbol: "GTC",
|
||||
contractAddress: "0xde30da39c46104798bb5aa3fe8b9e0e1f348163f",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Compound",
|
||||
symbol: "COMP",
|
||||
contractAddress: "0xc00e94cb662c3520282e6f5717214004a7f26888",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Aave Token",
|
||||
symbol: "AAVE",
|
||||
contractAddress: "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Uniswap",
|
||||
symbol: "UNI",
|
||||
contractAddress: "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Decentraland",
|
||||
symbol: "MANA",
|
||||
contractAddress: "0x0F5D2fB29fb7d3CFeE444a200298f468908cC942",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Storj",
|
||||
symbol: "STORJ",
|
||||
contractAddress: "0xb64ef51c888972c908cfacf59b47c1afbc0ab8ac",
|
||||
decimal: 8,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Maker",
|
||||
symbol: "MKR",
|
||||
contractAddress: "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Orchid",
|
||||
symbol: "OXT",
|
||||
contractAddress: "0x4575f41308EC1483f3d399aa9a2826d74Da13Deb",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Paxos Gold",
|
||||
symbol: "PAXG",
|
||||
contractAddress: "0x45804880De22913dAFE09f4980848ECE6EcbAf78",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Binance Coin",
|
||||
symbol: "BNB",
|
||||
contractAddress: "0xB8c77482e45F1F44dE1745F52C74426C631bDD52",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "stETH",
|
||||
symbol: "stETH",
|
||||
contractAddress: "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Lido DAO",
|
||||
symbol: "LDO",
|
||||
contractAddress: "0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Arbitrum",
|
||||
symbol: "ARB",
|
||||
contractAddress: "0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Graph Token",
|
||||
symbol: "GRT",
|
||||
contractAddress: "0xc944E90C64B2c07662A292be6244BDf05Cda44a7",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Frax",
|
||||
symbol: "FRAX",
|
||||
contractAddress: "0x853d955aCEf822Db058eb8505911ED77F175b99e",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Gemini dollar",
|
||||
symbol: "GUSD",
|
||||
contractAddress: "0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd",
|
||||
decimal: 2,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Compound Ether",
|
||||
symbol: "cETH",
|
||||
contractAddress: "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5",
|
||||
decimal: 8,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Binance USD",
|
||||
symbol: "BUSD",
|
||||
contractAddress: "0x4Fabb145d64652a948d72533023f6E7A623C7C53",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "TrueUSD",
|
||||
symbol: "TUSD",
|
||||
contractAddress: "0x0000000000085d4780B73119b644AE5ecd22b376",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Cronos Coin",
|
||||
symbol: "CRO",
|
||||
contractAddress: "0xA0b73E1Ff0B80914AB6fe0444E65848C4C34450b",
|
||||
decimal: 8,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Pax Dollar",
|
||||
symbol: "USDP",
|
||||
contractAddress: "0x8E870D67F660D95d5be530380D0eC0bd388289E1",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Fantom Token",
|
||||
symbol: "FTM",
|
||||
contractAddress: "0x4E15361FD6b4BB609Fa63C81A2be19d873717870",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "BitTorrent",
|
||||
symbol: "BTT",
|
||||
contractAddress: "0xC669928185DbCE49d2230CC9B0979BE6DC797957",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Nexo",
|
||||
symbol: "NEXO",
|
||||
contractAddress: "0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "dYdX",
|
||||
symbol: "DYDX",
|
||||
contractAddress: "0x92D6C1e31e14520e676a687F0a93788B716BEff5",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "PancakeSwap Token",
|
||||
symbol: "Cake",
|
||||
contractAddress: "0x152649eA73beAb28c5b49B26eb48f7EAD6d4c898",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "BAT",
|
||||
symbol: "BAT",
|
||||
contractAddress: "0x0D8775F648430679A709E98d2b0Cb6250d2887EF",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "1INCH Token",
|
||||
symbol: "1INCH",
|
||||
contractAddress: "0x111111111117dC0aa78b770fA6A738034120C302",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Ethereum Name Service",
|
||||
symbol: "ENS",
|
||||
contractAddress: "0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "ZRX",
|
||||
symbol: "ZRX",
|
||||
contractAddress: "0xE41d2489571d322189246DaFA5ebDe1F4699F498",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Verse",
|
||||
symbol: "VERSE",
|
||||
contractAddress: "0x249cA82617eC3DfB2589c4c17ab7EC9765350a18",
|
||||
decimal: 18,
|
||||
enabled: false,
|
||||
),
|
||||
];
|
||||
|
||||
List<Erc20Token> get initialErc20Tokens => _defaultTokens.map((token) {
|
||||
String? iconPath;
|
||||
try {
|
||||
iconPath = CryptoCurrency.all
|
||||
.firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
|
||||
.iconPath;
|
||||
} catch (_) {}
|
||||
|
||||
if (iconPath != null) {
|
||||
return Erc20Token.copyWith(token, iconPath);
|
||||
}
|
||||
|
||||
return token;
|
||||
}).toList();
|
||||
}
|
47
cw_ethereum/lib/erc20_balance.dart
Normal file
47
cw_ethereum/lib/erc20_balance.dart
Normal file
|
@ -0,0 +1,47 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cw_core/balance.dart';
|
||||
|
||||
class ERC20Balance extends Balance {
|
||||
ERC20Balance(this.balance, {this.exponent = 18})
|
||||
: super(balance.toInt(),
|
||||
balance.toInt());
|
||||
|
||||
final BigInt balance;
|
||||
final int exponent;
|
||||
|
||||
@override
|
||||
String get formattedAdditionalBalance {
|
||||
final String formattedBalance = (balance / BigInt.from(10).pow(exponent)).toString();
|
||||
return formattedBalance.substring(0, min(12, formattedBalance.length));
|
||||
}
|
||||
|
||||
@override
|
||||
String get formattedAvailableBalance {
|
||||
final String formattedBalance = (balance / BigInt.from(10).pow(exponent)).toString();
|
||||
return formattedBalance.substring(0, min(12, formattedBalance.length));
|
||||
}
|
||||
|
||||
String toJSON() => json.encode({
|
||||
'balanceInWei': balance.toString(),
|
||||
'exponent': exponent,
|
||||
});
|
||||
|
||||
static ERC20Balance? fromJSON(String? jsonSource) {
|
||||
if (jsonSource == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final decoded = json.decode(jsonSource) as Map;
|
||||
|
||||
try {
|
||||
return ERC20Balance(
|
||||
BigInt.parse(decoded['balanceInWei']),
|
||||
exponent: decoded['exponent'],
|
||||
);
|
||||
} catch (e) {
|
||||
return ERC20Balance(BigInt.zero);
|
||||
}
|
||||
}
|
||||
}
|
230
cw_ethereum/lib/ethereum_client.dart
Normal file
230
cw_ethereum/lib/ethereum_client.dart
Normal file
|
@ -0,0 +1,230 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_ethereum/erc20_balance.dart';
|
||||
import 'package:cw_core/erc20_token.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_model.dart';
|
||||
import 'package:cw_ethereum/pending_ethereum_transaction.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:web3dart/web3dart.dart';
|
||||
import 'package:web3dart/contracts/erc20.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_priority.dart';
|
||||
import 'package:cw_ethereum/.secrets.g.dart' as secrets;
|
||||
|
||||
class EthereumClient {
|
||||
final _httpClient = Client();
|
||||
Web3Client? _client;
|
||||
|
||||
bool connect(Node node) {
|
||||
try {
|
||||
_client = Web3Client(node.uri.toString(), _httpClient);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void setListeners(EthereumAddress userAddress, Function() onNewTransaction) async {
|
||||
// _client?.pendingTransactions().listen((transactionHash) async {
|
||||
// final transaction = await _client!.getTransactionByHash(transactionHash);
|
||||
//
|
||||
// if (transaction.from.hex == userAddress || transaction.to?.hex == userAddress) {
|
||||
// onNewTransaction();
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
Future<EtherAmount> getBalance(EthereumAddress address) async =>
|
||||
await _client!.getBalance(address);
|
||||
|
||||
Future<int> getGasUnitPrice() async {
|
||||
final gasPrice = await _client!.getGasPrice();
|
||||
return gasPrice.getInWei.toInt();
|
||||
}
|
||||
|
||||
Future<int> getEstimatedGas() async {
|
||||
final estimatedGas = await _client!.estimateGas();
|
||||
return estimatedGas.toInt();
|
||||
}
|
||||
|
||||
Future<PendingEthereumTransaction> signTransaction({
|
||||
required EthPrivateKey privateKey,
|
||||
required String toAddress,
|
||||
required String amount,
|
||||
required int gas,
|
||||
required EthereumTransactionPriority priority,
|
||||
required CryptoCurrency currency,
|
||||
required int exponent,
|
||||
String? contractAddress,
|
||||
}) async {
|
||||
assert(currency == CryptoCurrency.eth || contractAddress != null);
|
||||
|
||||
bool _isEthereum = currency == CryptoCurrency.eth;
|
||||
|
||||
final price = await _client!.getGasPrice();
|
||||
|
||||
final Transaction transaction = Transaction(
|
||||
from: privateKey.address,
|
||||
to: EthereumAddress.fromHex(toAddress),
|
||||
maxGas: gas,
|
||||
gasPrice: price,
|
||||
maxPriorityFeePerGas: EtherAmount.fromUnitAndValue(EtherUnit.gwei, priority.tip),
|
||||
value: _isEthereum ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(),
|
||||
);
|
||||
|
||||
final signedTransaction = await _client!.signTransaction(privateKey, transaction);
|
||||
|
||||
final Function _sendTransaction;
|
||||
|
||||
if (_isEthereum) {
|
||||
_sendTransaction = () async => await sendTransaction(signedTransaction);
|
||||
} else {
|
||||
final erc20 = Erc20(
|
||||
client: _client!,
|
||||
address: EthereumAddress.fromHex(contractAddress!),
|
||||
);
|
||||
|
||||
_sendTransaction = () async {
|
||||
await erc20.transfer(
|
||||
EthereumAddress.fromHex(toAddress),
|
||||
BigInt.parse(amount),
|
||||
credentials: privateKey,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
return PendingEthereumTransaction(
|
||||
signedTransaction: signedTransaction,
|
||||
amount: amount,
|
||||
fee: BigInt.from(gas) * price.getInWei,
|
||||
sendTransaction: _sendTransaction,
|
||||
exponent: exponent,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> sendTransaction(Uint8List signedTransaction) async =>
|
||||
await _client!.sendRawTransaction(signedTransaction);
|
||||
|
||||
Future getTransactionDetails(String transactionHash) async {
|
||||
// Wait for the transaction receipt to become available
|
||||
TransactionReceipt? receipt;
|
||||
while (receipt == null) {
|
||||
receipt = await _client!.getTransactionReceipt(transactionHash);
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
}
|
||||
|
||||
// Print the receipt information
|
||||
print('Transaction Hash: ${receipt.transactionHash}');
|
||||
print('Block Hash: ${receipt.blockHash}');
|
||||
print('Block Number: ${receipt.blockNumber}');
|
||||
print('Gas Used: ${receipt.gasUsed}');
|
||||
|
||||
/*
|
||||
Transaction Hash: [112, 244, 4, 238, 89, 199, 171, 191, 210, 236, 110, 42, 185, 202, 220, 21, 27, 132, 123, 221, 137, 90, 77, 13, 23, 43, 12, 230, 93, 63, 221, 116]
|
||||
I/flutter ( 4474): Block Hash: [149, 44, 250, 119, 111, 104, 82, 98, 17, 89, 30, 190, 25, 44, 218, 118, 127, 189, 241, 35, 213, 106, 25, 95, 195, 37, 55, 131, 185, 180, 246, 200]
|
||||
I/flutter ( 4474): Block Number: 17120242
|
||||
I/flutter ( 4474): Gas Used: 21000
|
||||
*/
|
||||
|
||||
// Wait for the transaction receipt to become available
|
||||
TransactionInformation? transactionInformation;
|
||||
while (transactionInformation == null) {
|
||||
print("********************************");
|
||||
transactionInformation = await _client!.getTransactionByHash(transactionHash);
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
}
|
||||
// Print the receipt information
|
||||
print('Transaction Hash: ${transactionInformation.hash}');
|
||||
print('Block Hash: ${transactionInformation.blockHash}');
|
||||
print('Block Number: ${transactionInformation.blockNumber}');
|
||||
print('Gas Used: ${transactionInformation.gas}');
|
||||
|
||||
/*
|
||||
Transaction Hash: 0x70f404ee59c7abbfd2ec6e2ab9cadc151b847bdd895a4d0d172b0ce65d3fdd74
|
||||
I/flutter ( 4474): Block Hash: 0x952cfa776f68526211591ebe192cda767fbdf123d56a195fc3253783b9b4f6c8
|
||||
I/flutter ( 4474): Block Number: 17120242
|
||||
I/flutter ( 4474): Gas Used: 53000
|
||||
*/
|
||||
}
|
||||
|
||||
Future<ERC20Balance> fetchERC20Balances(
|
||||
EthereumAddress userAddress, String contractAddress) async {
|
||||
final erc20 = Erc20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
|
||||
final balance = await erc20.balanceOf(userAddress);
|
||||
|
||||
int exponent = (await erc20.decimals()).toInt();
|
||||
|
||||
return ERC20Balance(balance, exponent: exponent);
|
||||
}
|
||||
|
||||
Future<Erc20Token?> getErc20Token(String contractAddress) async {
|
||||
try {
|
||||
final erc20 = Erc20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
|
||||
final name = await erc20.name();
|
||||
final symbol = await erc20.symbol();
|
||||
final decimal = await erc20.decimals();
|
||||
|
||||
return Erc20Token(
|
||||
name: name,
|
||||
symbol: symbol,
|
||||
contractAddress: contractAddress,
|
||||
decimal: decimal.toInt(),
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
_client?.dispose();
|
||||
}
|
||||
|
||||
Future<List<EthereumTransactionModel>> fetchTransactions(String address,
|
||||
{String? contractAddress}) async {
|
||||
try {
|
||||
final response = await _httpClient.get(Uri.https("api.etherscan.io", "/api", {
|
||||
"module": "account",
|
||||
"action": contractAddress != null ? "tokentx" : "txlist",
|
||||
if (contractAddress != null) "contractaddress": contractAddress,
|
||||
"address": address,
|
||||
"apikey": secrets.etherScanApiKey,
|
||||
}));
|
||||
|
||||
final _jsonResponse = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300 && _jsonResponse['status'] != 0) {
|
||||
return (_jsonResponse['result'] as List)
|
||||
.map((e) => EthereumTransactionModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (e) {
|
||||
print(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Future<int> _getDecimalPlacesForContract(DeployedContract contract) async {
|
||||
// final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json");
|
||||
// final contractAbi = ContractAbi.fromJson(abi, "ERC20");
|
||||
//
|
||||
// final contract = DeployedContract(
|
||||
// contractAbi,
|
||||
// EthereumAddress.fromHex(_erc20Currencies[erc20Currency]!),
|
||||
// );
|
||||
// final decimalsFunction = contract.function('decimals');
|
||||
// final decimals = await _client!.call(
|
||||
// contract: contract,
|
||||
// function: decimalsFunction,
|
||||
// params: [],
|
||||
// );
|
||||
//
|
||||
// int exponent = int.parse(decimals.first.toString());
|
||||
// return exponent;
|
||||
// }
|
||||
}
|
11
cw_ethereum/lib/ethereum_exceptions.dart
Normal file
11
cw_ethereum/lib/ethereum_exceptions.dart
Normal file
|
@ -0,0 +1,11 @@
|
|||
import 'package:cw_core/crypto_currency.dart';
|
||||
|
||||
class EthereumTransactionCreationException implements Exception {
|
||||
final String exceptionMessage;
|
||||
|
||||
EthereumTransactionCreationException(CryptoCurrency currency) :
|
||||
this.exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.';
|
||||
|
||||
@override
|
||||
String toString() => exceptionMessage;
|
||||
}
|
25
cw_ethereum/lib/ethereum_formatter.dart
Normal file
25
cw_ethereum/lib/ethereum_formatter.dart
Normal file
|
@ -0,0 +1,25 @@
|
|||
import 'package:intl/intl.dart';
|
||||
|
||||
const ethereumAmountLength = 12;
|
||||
const ethereumAmountDivider = 1000000000000;
|
||||
final ethereumAmountFormat = NumberFormat()
|
||||
..maximumFractionDigits = ethereumAmountLength
|
||||
..minimumFractionDigits = 1;
|
||||
|
||||
class EthereumFormatter {
|
||||
static int parseEthereumAmount(String amount) {
|
||||
try {
|
||||
return (double.parse(amount) * ethereumAmountDivider).round();
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static double parseEthereumAmountToDouble(int amount) {
|
||||
try {
|
||||
return amount / ethereumAmountDivider;
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
2058
cw_ethereum/lib/ethereum_mnemonics.dart
Normal file
2058
cw_ethereum/lib/ethereum_mnemonics.dart
Normal file
File diff suppressed because it is too large
Load diff
17
cw_ethereum/lib/ethereum_transaction_credentials.dart
Normal file
17
cw_ethereum/lib/ethereum_transaction_credentials.dart
Normal file
|
@ -0,0 +1,17 @@
|
|||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/output_info.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_priority.dart';
|
||||
|
||||
class EthereumTransactionCredentials {
|
||||
EthereumTransactionCredentials(
|
||||
this.outputs, {
|
||||
required this.priority,
|
||||
required this.currency,
|
||||
this.feeRate,
|
||||
});
|
||||
|
||||
final List<OutputInfo> outputs;
|
||||
final EthereumTransactionPriority? priority;
|
||||
final int? feeRate;
|
||||
final CryptoCurrency currency;
|
||||
}
|
77
cw_ethereum/lib/ethereum_transaction_history.dart
Normal file
77
cw_ethereum/lib/ethereum_transaction_history.dart
Normal file
|
@ -0,0 +1,77 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:core';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_ethereum/file.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:cw_core/transaction_history.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_info.dart';
|
||||
|
||||
part 'ethereum_transaction_history.g.dart';
|
||||
|
||||
const transactionsHistoryFileName = 'transactions.json';
|
||||
|
||||
class EthereumTransactionHistory = EthereumTransactionHistoryBase with _$EthereumTransactionHistory;
|
||||
|
||||
abstract class EthereumTransactionHistoryBase
|
||||
extends TransactionHistoryBase<EthereumTransactionInfo> with Store {
|
||||
EthereumTransactionHistoryBase({required this.walletInfo, required String password})
|
||||
: _password = password {
|
||||
transactions = ObservableMap<String, EthereumTransactionInfo>();
|
||||
}
|
||||
|
||||
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 data = json.encode({'transactions': transactions});
|
||||
await writeData(path: path, password: _password, data: data);
|
||||
} catch (e, s) {
|
||||
print('Error while save ethereum transaction history: ${e.toString()}');
|
||||
print(s);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void addOne(EthereumTransactionInfo transaction) => transactions[transaction.id] = transaction;
|
||||
|
||||
@override
|
||||
void addMany(Map<String, EthereumTransactionInfo> 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 = EthereumTransactionInfo.fromJson(val);
|
||||
_update(tx);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
||||
void _update(EthereumTransactionInfo transaction) => transactions[transaction.id] = transaction;
|
||||
}
|
74
cw_ethereum/lib/ethereum_transaction_info.dart
Normal file
74
cw_ethereum/lib/ethereum_transaction_info.dart
Normal file
|
@ -0,0 +1,74 @@
|
|||
import 'package:cw_core/format_amount.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
|
||||
class EthereumTransactionInfo extends TransactionInfo {
|
||||
EthereumTransactionInfo({
|
||||
required this.id,
|
||||
required this.height,
|
||||
required this.ethAmount,
|
||||
required this.ethFee,
|
||||
this.tokenSymbol = "ETH",
|
||||
this.exponent = 18,
|
||||
required this.direction,
|
||||
required this.isPending,
|
||||
required this.date,
|
||||
required this.confirmations,
|
||||
}) : this.amount = ethAmount.toInt(),
|
||||
this.fee = ethFee.toInt();
|
||||
|
||||
final String id;
|
||||
final int height;
|
||||
final int amount;
|
||||
final BigInt ethAmount;
|
||||
final int exponent;
|
||||
final TransactionDirection direction;
|
||||
final DateTime date;
|
||||
final bool isPending;
|
||||
final int fee;
|
||||
final BigInt ethFee;
|
||||
final int confirmations;
|
||||
final String tokenSymbol;
|
||||
String? _fiatAmount;
|
||||
|
||||
@override
|
||||
String amountFormatted() =>
|
||||
'${formatAmount((ethAmount / BigInt.from(10).pow(exponent)).toString())} $tokenSymbol';
|
||||
|
||||
@override
|
||||
String fiatAmount() => _fiatAmount ?? '';
|
||||
|
||||
@override
|
||||
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
|
||||
|
||||
@override
|
||||
String feeFormatted() => '${(ethFee / BigInt.from(10).pow(18)).toString()} ETH';
|
||||
|
||||
factory EthereumTransactionInfo.fromJson(Map<String, dynamic> data) {
|
||||
return EthereumTransactionInfo(
|
||||
id: data['id'] as String,
|
||||
height: data['height'] as int,
|
||||
ethAmount: BigInt.parse(data['amount']),
|
||||
exponent: data['exponent'] as int,
|
||||
ethFee: BigInt.parse(data['fee']),
|
||||
direction: parseTransactionDirectionFromInt(data['direction'] as int),
|
||||
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
|
||||
isPending: data['isPending'] as bool,
|
||||
confirmations: data['confirmations'] as int,
|
||||
tokenSymbol: data['tokenSymbol'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'height': height,
|
||||
'amount': ethAmount.toString(),
|
||||
'exponent': exponent,
|
||||
'fee': ethFee.toString(),
|
||||
'direction': direction.index,
|
||||
'date': date.millisecondsSinceEpoch,
|
||||
'isPending': isPending,
|
||||
'confirmations': confirmations,
|
||||
'tokenSymbol': tokenSymbol,
|
||||
};
|
||||
}
|
47
cw_ethereum/lib/ethereum_transaction_model.dart
Normal file
47
cw_ethereum/lib/ethereum_transaction_model.dart
Normal file
|
@ -0,0 +1,47 @@
|
|||
class EthereumTransactionModel {
|
||||
final DateTime date;
|
||||
final String hash;
|
||||
final String from;
|
||||
final String to;
|
||||
final BigInt amount;
|
||||
final int gasUsed;
|
||||
final BigInt gasPrice;
|
||||
final String contractAddress;
|
||||
final int confirmations;
|
||||
final int blockNumber;
|
||||
final String? tokenSymbol;
|
||||
final int? tokenDecimal;
|
||||
final bool isError;
|
||||
|
||||
EthereumTransactionModel({
|
||||
required this.date,
|
||||
required this.hash,
|
||||
required this.from,
|
||||
required this.to,
|
||||
required this.amount,
|
||||
required this.gasUsed,
|
||||
required this.gasPrice,
|
||||
required this.contractAddress,
|
||||
required this.confirmations,
|
||||
required this.blockNumber,
|
||||
required this.tokenSymbol,
|
||||
required this.tokenDecimal,
|
||||
required this.isError,
|
||||
});
|
||||
|
||||
factory EthereumTransactionModel.fromJson(Map<String, dynamic> json) => EthereumTransactionModel(
|
||||
date: DateTime.fromMillisecondsSinceEpoch(int.parse(json["timeStamp"]) * 1000),
|
||||
hash: json["hash"],
|
||||
from: json["from"],
|
||||
to: json["to"],
|
||||
amount: BigInt.parse(json["value"]),
|
||||
gasUsed: int.parse(json["gasUsed"]),
|
||||
gasPrice: BigInt.parse(json["gasPrice"]),
|
||||
contractAddress: json["contractAddress"],
|
||||
confirmations: int.parse(json["confirmations"]),
|
||||
blockNumber: int.parse(json["blockNumber"]),
|
||||
tokenSymbol: json["tokenSymbol"] ?? "ETH",
|
||||
tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""),
|
||||
isError: json["isError"] == "1",
|
||||
);
|
||||
}
|
52
cw_ethereum/lib/ethereum_transaction_priority.dart
Normal file
52
cw_ethereum/lib/ethereum_transaction_priority.dart
Normal file
|
@ -0,0 +1,52 @@
|
|||
import 'package:cw_core/transaction_priority.dart';
|
||||
|
||||
class EthereumTransactionPriority extends TransactionPriority {
|
||||
final int tip;
|
||||
|
||||
const EthereumTransactionPriority({required String title, required int raw, required this.tip})
|
||||
: super(title: title, raw: raw);
|
||||
|
||||
static const List<EthereumTransactionPriority> all = [fast, medium, slow];
|
||||
static const EthereumTransactionPriority slow =
|
||||
EthereumTransactionPriority(title: 'slow', raw: 0, tip: 1);
|
||||
static const EthereumTransactionPriority medium =
|
||||
EthereumTransactionPriority(title: 'Medium', raw: 1, tip: 2);
|
||||
static const EthereumTransactionPriority fast =
|
||||
EthereumTransactionPriority(title: 'Fast', raw: 2, tip: 4);
|
||||
|
||||
static EthereumTransactionPriority deserialize({required int raw}) {
|
||||
switch (raw) {
|
||||
case 0:
|
||||
return slow;
|
||||
case 1:
|
||||
return medium;
|
||||
case 2:
|
||||
return fast;
|
||||
default:
|
||||
throw Exception('Unexpected token: $raw for EthereumTransactionPriority deserialize');
|
||||
}
|
||||
}
|
||||
|
||||
String get units => 'gas';
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var label = '';
|
||||
|
||||
switch (this) {
|
||||
case EthereumTransactionPriority.slow:
|
||||
label = 'Slow';
|
||||
break;
|
||||
case EthereumTransactionPriority.medium:
|
||||
label = 'Medium';
|
||||
break;
|
||||
case EthereumTransactionPriority.fast:
|
||||
label = 'Fast';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
}
|
473
cw_ethereum/lib/ethereum_wallet.dart
Normal file
473
cw_ethereum/lib/ethereum_wallet.dart
Normal file
|
@ -0,0 +1,473 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
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_ethereum/default_erc20_tokens.dart';
|
||||
import 'package:cw_ethereum/erc20_balance.dart';
|
||||
import 'package:cw_ethereum/ethereum_client.dart';
|
||||
import 'package:cw_ethereum/ethereum_exceptions.dart';
|
||||
import 'package:cw_ethereum/ethereum_formatter.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_credentials.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_history.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_info.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_model.dart';
|
||||
import 'package:cw_ethereum/ethereum_transaction_priority.dart';
|
||||
import 'package:cw_ethereum/ethereum_wallet_addresses.dart';
|
||||
import 'package:cw_ethereum/file.dart';
|
||||
import 'package:cw_core/erc20_token.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hex/hex.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:web3dart/web3dart.dart';
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:bip32/bip32.dart' as bip32;
|
||||
|
||||
part 'ethereum_wallet.g.dart';
|
||||
|
||||
class EthereumWallet = EthereumWalletBase with _$EthereumWallet;
|
||||
|
||||
abstract class EthereumWalletBase
|
||||
extends WalletBase<ERC20Balance, EthereumTransactionHistory, EthereumTransactionInfo>
|
||||
with Store {
|
||||
EthereumWalletBase({
|
||||
required WalletInfo walletInfo,
|
||||
required String mnemonic,
|
||||
required String password,
|
||||
ERC20Balance? initialBalance,
|
||||
}) : syncStatus = NotConnectedSyncStatus(),
|
||||
_password = password,
|
||||
_mnemonic = mnemonic,
|
||||
_isTransactionUpdating = false,
|
||||
_client = EthereumClient(),
|
||||
walletAddresses = EthereumWalletAddresses(walletInfo),
|
||||
balance = ObservableMap<CryptoCurrency, ERC20Balance>.of(
|
||||
{CryptoCurrency.eth: initialBalance ?? ERC20Balance(BigInt.zero)}),
|
||||
super(walletInfo) {
|
||||
this.walletInfo = walletInfo;
|
||||
transactionHistory = EthereumTransactionHistory(walletInfo: walletInfo, password: password);
|
||||
|
||||
if (!Hive.isAdapterRegistered(Erc20Token.typeId)) {
|
||||
Hive.registerAdapter(Erc20TokenAdapter());
|
||||
}
|
||||
|
||||
_sharedPrefs.complete(SharedPreferences.getInstance());
|
||||
}
|
||||
|
||||
final String _mnemonic;
|
||||
final String _password;
|
||||
|
||||
late final Box<Erc20Token> erc20TokensBox;
|
||||
|
||||
late final EthPrivateKey _privateKey;
|
||||
|
||||
late EthereumClient _client;
|
||||
|
||||
int? _gasPrice;
|
||||
int? _estimatedGas;
|
||||
bool _isTransactionUpdating;
|
||||
|
||||
// TODO: remove after integrating our own node and having eth_newPendingTransactionFilter
|
||||
Timer? _transactionsUpdateTimer;
|
||||
|
||||
@override
|
||||
WalletAddresses walletAddresses;
|
||||
|
||||
@override
|
||||
@observable
|
||||
SyncStatus syncStatus;
|
||||
|
||||
@override
|
||||
@observable
|
||||
late ObservableMap<CryptoCurrency, ERC20Balance> balance;
|
||||
|
||||
Completer<SharedPreferences> _sharedPrefs = Completer();
|
||||
|
||||
Future<void> init() async {
|
||||
erc20TokensBox = await Hive.openBox<Erc20Token>(Erc20Token.boxName);
|
||||
await walletAddresses.init();
|
||||
await transactionHistory.init();
|
||||
_privateKey = await getPrivateKey(_mnemonic, _password);
|
||||
walletAddresses.address = _privateKey.address.toString();
|
||||
await save();
|
||||
}
|
||||
|
||||
@override
|
||||
int calculateEstimatedFee(TransactionPriority priority, int? amount) {
|
||||
try {
|
||||
if (priority is EthereumTransactionPriority) {
|
||||
final priorityFee =
|
||||
EtherAmount.fromUnitAndValue(EtherUnit.gwei, priority.tip).getInWei.toInt();
|
||||
return (_gasPrice! + priorityFee) * (_estimatedGas ?? 0);
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (e) {
|
||||
return 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("Ethereum Node connection failed");
|
||||
}
|
||||
|
||||
_client.setListeners(_privateKey.address, _onNewTransaction);
|
||||
|
||||
_setTransactionUpdateTimer();
|
||||
|
||||
syncStatus = ConnectedSyncStatus();
|
||||
} catch (e) {
|
||||
syncStatus = FailedSyncStatus();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PendingTransaction> createTransaction(Object credentials) async {
|
||||
final _credentials = credentials as EthereumTransactionCredentials;
|
||||
final outputs = _credentials.outputs;
|
||||
final hasMultiDestination = outputs.length > 1;
|
||||
final _erc20Balance = balance[_credentials.currency]!;
|
||||
BigInt totalAmount = BigInt.zero;
|
||||
int exponent =
|
||||
_credentials.currency is Erc20Token ? (_credentials.currency as Erc20Token).decimal : 18;
|
||||
num amountToEthereumMultiplier = pow(10, exponent);
|
||||
|
||||
if (hasMultiDestination) {
|
||||
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
|
||||
throw EthereumTransactionCreationException(_credentials.currency);
|
||||
}
|
||||
|
||||
final totalOriginalAmount = EthereumFormatter.parseEthereumAmountToDouble(
|
||||
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
|
||||
totalAmount = BigInt.from(totalOriginalAmount * amountToEthereumMultiplier);
|
||||
|
||||
if (_erc20Balance.balance < totalAmount) {
|
||||
throw EthereumTransactionCreationException(_credentials.currency);
|
||||
}
|
||||
} else {
|
||||
final output = outputs.first;
|
||||
final BigInt allAmount =
|
||||
_erc20Balance.balance - BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
|
||||
final totalOriginalAmount =
|
||||
EthereumFormatter.parseEthereumAmountToDouble(output.formattedCryptoAmount ?? 0);
|
||||
totalAmount = output.sendAll
|
||||
? allAmount
|
||||
: BigInt.from(totalOriginalAmount * amountToEthereumMultiplier);
|
||||
|
||||
if (_erc20Balance.balance < totalAmount) {
|
||||
throw EthereumTransactionCreationException(_credentials.currency);
|
||||
}
|
||||
}
|
||||
|
||||
final pendingEthereumTransaction = await _client.signTransaction(
|
||||
privateKey: _privateKey,
|
||||
toAddress: _credentials.outputs.first.address,
|
||||
amount: totalAmount.toString(),
|
||||
gas: _estimatedGas!,
|
||||
priority: _credentials.priority!,
|
||||
currency: _credentials.currency,
|
||||
exponent: exponent,
|
||||
contractAddress: _credentials.currency is Erc20Token
|
||||
? (_credentials.currency as Erc20Token).contractAddress
|
||||
: null,
|
||||
);
|
||||
|
||||
return pendingEthereumTransaction;
|
||||
}
|
||||
|
||||
Future<void> _updateTransactions() async {
|
||||
try {
|
||||
if (_isTransactionUpdating) {
|
||||
return;
|
||||
}
|
||||
bool isEtherscanEnabled = (await _sharedPrefs.future).getBool("use_etherscan") ?? true;
|
||||
if (!isEtherscanEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isTransactionUpdating = true;
|
||||
final transactions = await fetchTransactions();
|
||||
transactionHistory.addMany(transactions);
|
||||
await transactionHistory.save();
|
||||
_isTransactionUpdating = false;
|
||||
} catch (_) {
|
||||
_isTransactionUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, EthereumTransactionInfo>> fetchTransactions() async {
|
||||
final address = _privateKey.address.hex;
|
||||
final transactions = await _client.fetchTransactions(address);
|
||||
|
||||
final List<Future<List<EthereumTransactionModel>>> erc20TokensTransactions = [];
|
||||
|
||||
for (var token in balance.keys) {
|
||||
if (token is Erc20Token) {
|
||||
erc20TokensTransactions.add(_client.fetchTransactions(
|
||||
address,
|
||||
contractAddress: token.contractAddress,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
final tokensTransaction = await Future.wait(erc20TokensTransactions);
|
||||
transactions.addAll(tokensTransaction.expand((element) => element));
|
||||
|
||||
final Map<String, EthereumTransactionInfo> result = {};
|
||||
|
||||
for (var transactionModel in transactions) {
|
||||
if (transactionModel.isError) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result[transactionModel.hash] = EthereumTransactionInfo(
|
||||
id: transactionModel.hash,
|
||||
height: transactionModel.blockNumber,
|
||||
ethAmount: transactionModel.amount,
|
||||
direction: transactionModel.from == address
|
||||
? TransactionDirection.outgoing
|
||||
: TransactionDirection.incoming,
|
||||
isPending: false,
|
||||
date: transactionModel.date,
|
||||
confirmations: transactionModel.confirmations,
|
||||
ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice,
|
||||
exponent: transactionModel.tokenDecimal ?? 18,
|
||||
tokenSymbol: transactionModel.tokenSymbol ?? "ETH",
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Object get keys => throw UnimplementedError("keys");
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
@override
|
||||
String get seed => _mnemonic;
|
||||
|
||||
@action
|
||||
@override
|
||||
Future<void> startSync() async {
|
||||
try {
|
||||
syncStatus = AttemptingSyncStatus();
|
||||
await _updateBalance();
|
||||
await _updateTransactions();
|
||||
_gasPrice = await _client.getGasUnitPrice();
|
||||
_estimatedGas = await _client.getEstimatedGas();
|
||||
|
||||
Timer.periodic(
|
||||
const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasUnitPrice());
|
||||
Timer.periodic(const Duration(seconds: 10),
|
||||
(timer) async => _estimatedGas = await _client.getEstimatedGas());
|
||||
|
||||
syncStatus = SyncedSyncStatus();
|
||||
} catch (e) {
|
||||
syncStatus = FailedSyncStatus();
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
|
||||
|
||||
String toJSON() => json.encode({
|
||||
'mnemonic': _mnemonic,
|
||||
'balance': balance[currency]!.toJSON(),
|
||||
});
|
||||
|
||||
static Future<EthereumWallet> 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 balance = ERC20Balance.fromJSON(data['balance'] as String) ?? ERC20Balance(BigInt.zero);
|
||||
|
||||
return EthereumWallet(
|
||||
walletInfo: walletInfo,
|
||||
password: password,
|
||||
mnemonic: mnemonic,
|
||||
initialBalance: balance,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updateBalance() async {
|
||||
balance[currency] = await _fetchEthBalance();
|
||||
|
||||
await _fetchErc20Balances();
|
||||
await save();
|
||||
}
|
||||
|
||||
Future<ERC20Balance> _fetchEthBalance() async {
|
||||
final balance = await _client.getBalance(_privateKey.address);
|
||||
return ERC20Balance(balance.getInWei);
|
||||
}
|
||||
|
||||
Future<void> _fetchErc20Balances() async {
|
||||
for (var token in erc20TokensBox.values) {
|
||||
try {
|
||||
if (token.enabled) {
|
||||
balance[token] = await _client.fetchERC20Balances(
|
||||
_privateKey.address,
|
||||
token.contractAddress,
|
||||
);
|
||||
} else {
|
||||
balance.remove(token);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
Future<EthPrivateKey> getPrivateKey(String mnemonic, String password) async {
|
||||
final seed = bip39.mnemonicToSeed(mnemonic);
|
||||
|
||||
final root = bip32.BIP32.fromSeed(seed);
|
||||
|
||||
const _hdPathEthereum = "m/44'/60'/0'/0";
|
||||
const index = 0;
|
||||
final addressAtIndex = root.derivePath("$_hdPathEthereum/$index");
|
||||
|
||||
return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List<int>));
|
||||
}
|
||||
|
||||
Future<void>? updateBalance() async => await _updateBalance();
|
||||
|
||||
List<Erc20Token> get erc20Currencies => erc20TokensBox.values.toList();
|
||||
|
||||
Future<void> addErc20Token(Erc20Token token) async {
|
||||
String? iconPath;
|
||||
try {
|
||||
iconPath = CryptoCurrency.all
|
||||
.firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
|
||||
.iconPath;
|
||||
} catch (_) {}
|
||||
|
||||
final _token = Erc20Token(
|
||||
name: token.name,
|
||||
symbol: token.symbol,
|
||||
contractAddress: token.contractAddress,
|
||||
decimal: token.decimal,
|
||||
enabled: token.enabled,
|
||||
iconPath: iconPath,
|
||||
);
|
||||
|
||||
await erc20TokensBox.put(_token.contractAddress, _token);
|
||||
|
||||
if (_token.enabled) {
|
||||
balance[_token] = await _client.fetchERC20Balances(
|
||||
_privateKey.address,
|
||||
_token.contractAddress,
|
||||
);
|
||||
} else {
|
||||
balance.remove(_token);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteErc20Token(Erc20Token token) async {
|
||||
await token.delete();
|
||||
|
||||
balance.remove(token);
|
||||
_updateBalance();
|
||||
}
|
||||
|
||||
Future<Erc20Token?> getErc20Token(String contractAddress) async =>
|
||||
await _client.getErc20Token(contractAddress);
|
||||
|
||||
void _onNewTransaction() {
|
||||
_updateBalance();
|
||||
_updateTransactions();
|
||||
}
|
||||
|
||||
void addInitialTokens() {
|
||||
final initialErc20Tokens = DefaultErc20Tokens().initialErc20Tokens;
|
||||
|
||||
initialErc20Tokens.forEach((token) => erc20TokensBox.put(token.contractAddress, token));
|
||||
}
|
||||
|
||||
@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(Duration(seconds: 10), (_) {
|
||||
_updateTransactions();
|
||||
_updateBalance();
|
||||
});
|
||||
}
|
||||
|
||||
void updateEtherscanUsageState(bool isEnabled) {
|
||||
if (isEnabled) {
|
||||
_updateTransactions();
|
||||
_setTransactionUpdateTimer();
|
||||
} else {
|
||||
_transactionsUpdateTimer?.cancel();
|
||||
}
|
||||
}
|
||||
}
|
33
cw_ethereum/lib/ethereum_wallet_addresses.dart
Normal file
33
cw_ethereum/lib/ethereum_wallet_addresses.dart
Normal 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 'ethereum_wallet_addresses.g.dart';
|
||||
|
||||
class EthereumWalletAddresses = EthereumWalletAddressesBase with _$EthereumWalletAddresses;
|
||||
|
||||
abstract class EthereumWalletAddressesBase extends WalletAddresses with Store {
|
||||
EthereumWalletAddressesBase(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());
|
||||
}
|
||||
}
|
||||
}
|
23
cw_ethereum/lib/ethereum_wallet_creation_credentials.dart
Normal file
23
cw_ethereum/lib/ethereum_wallet_creation_credentials.dart
Normal file
|
@ -0,0 +1,23 @@
|
|||
import 'package:cw_core/wallet_credentials.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
|
||||
class EthereumNewWalletCredentials extends WalletCredentials {
|
||||
EthereumNewWalletCredentials({required String name, WalletInfo? walletInfo})
|
||||
: super(name: name, walletInfo: walletInfo);
|
||||
}
|
||||
|
||||
class EthereumRestoreWalletFromSeedCredentials extends WalletCredentials {
|
||||
EthereumRestoreWalletFromSeedCredentials(
|
||||
{required String name, required String password, required this.mnemonic, WalletInfo? walletInfo})
|
||||
: super(name: name, password: password, walletInfo: walletInfo);
|
||||
|
||||
final String mnemonic;
|
||||
}
|
||||
|
||||
class EthereumRestoreWalletFromWIFCredentials extends WalletCredentials {
|
||||
EthereumRestoreWalletFromWIFCredentials(
|
||||
{required String name, required String password, required this.wif, WalletInfo? walletInfo})
|
||||
: super(name: name, password: password, walletInfo: walletInfo);
|
||||
|
||||
final String wif;
|
||||
}
|
108
cw_ethereum/lib/ethereum_wallet_service.dart
Normal file
108
cw_ethereum/lib/ethereum_wallet_service.dart
Normal file
|
@ -0,0 +1,108 @@
|
|||
import 'dart:io';
|
||||
|
||||
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_ethereum/ethereum_mnemonics.dart';
|
||||
import 'package:cw_ethereum/ethereum_wallet.dart';
|
||||
import 'package:cw_ethereum/ethereum_wallet_creation_credentials.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
class EthereumWalletService extends WalletService<EthereumNewWalletCredentials,
|
||||
EthereumRestoreWalletFromSeedCredentials, EthereumRestoreWalletFromWIFCredentials> {
|
||||
EthereumWalletService(this.walletInfoSource);
|
||||
|
||||
final Box<WalletInfo> walletInfoSource;
|
||||
|
||||
@override
|
||||
Future<EthereumWallet> create(EthereumNewWalletCredentials credentials) async {
|
||||
final mnemonic = bip39.generateMnemonic();
|
||||
final wallet = EthereumWallet(
|
||||
walletInfo: credentials.walletInfo!,
|
||||
mnemonic: mnemonic,
|
||||
password: credentials.password!,
|
||||
);
|
||||
|
||||
await wallet.init();
|
||||
wallet.addInitialTokens();
|
||||
await wallet.save();
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
@override
|
||||
WalletType getType() => WalletType.ethereum;
|
||||
|
||||
@override
|
||||
Future<bool> isWalletExit(String name) async =>
|
||||
File(await pathForWallet(name: name, type: getType())).existsSync();
|
||||
|
||||
@override
|
||||
Future<EthereumWallet> openWallet(String name, String password) async {
|
||||
final walletInfo =
|
||||
walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
|
||||
final wallet = await EthereumWalletBase.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<EthereumWallet> restoreFromKeys(credentials) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<EthereumWallet> restoreFromSeed(
|
||||
EthereumRestoreWalletFromSeedCredentials credentials) async {
|
||||
if (!bip39.validateMnemonic(credentials.mnemonic)) {
|
||||
throw EthereumMnemonicIsIncorrectException();
|
||||
}
|
||||
|
||||
final wallet = EthereumWallet(
|
||||
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 EthereumWalletBase.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);
|
||||
}
|
||||
}
|
39
cw_ethereum/lib/file.dart
Normal file
39
cw_ethereum/lib/file.dart
Normal 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);
|
||||
}
|
36
cw_ethereum/lib/pending_ethereum_transaction.dart
Normal file
36
cw_ethereum/lib/pending_ethereum_transaction.dart
Normal file
|
@ -0,0 +1,36 @@
|
|||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:web3dart/crypto.dart';
|
||||
|
||||
class PendingEthereumTransaction with PendingTransaction {
|
||||
final Function sendTransaction;
|
||||
final Uint8List signedTransaction;
|
||||
final BigInt fee;
|
||||
final String amount;
|
||||
final int exponent;
|
||||
|
||||
PendingEthereumTransaction({
|
||||
required this.sendTransaction,
|
||||
required this.signedTransaction,
|
||||
required this.fee,
|
||||
required this.amount,
|
||||
required this.exponent,
|
||||
});
|
||||
|
||||
@override
|
||||
String get amountFormatted => (BigInt.parse(amount) / BigInt.from(pow(10, exponent))).toString();
|
||||
|
||||
@override
|
||||
Future<void> commit() async => await sendTransaction();
|
||||
|
||||
@override
|
||||
String get feeFormatted => (fee / BigInt.from(pow(10, 18))).toString();
|
||||
|
||||
@override
|
||||
String get hex => bytesToHex(signedTransaction, include0x: true);
|
||||
|
||||
@override
|
||||
String get id => '';
|
||||
}
|
68
cw_ethereum/pubspec.yaml
Normal file
68
cw_ethereum/pubspec.yaml
Normal file
|
@ -0,0 +1,68 @@
|
|||
name: cw_ethereum
|
||||
description: A new Flutter package project.
|
||||
version: 0.0.1
|
||||
publish_to: none
|
||||
author: Cake Wallet
|
||||
homepage: https://cakewallet.com
|
||||
|
||||
environment:
|
||||
sdk: '>=2.18.2 <3.0.0'
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
web3dart: 2.3.5
|
||||
mobx: ^2.0.7+4
|
||||
bip39: ^1.0.6
|
||||
bip32: ^2.0.0
|
||||
ed25519_hd_key: ^2.2.0
|
||||
hex: ^0.2.0
|
||||
http: ^0.13.4
|
||||
shared_preferences: ^2.0.15
|
||||
cw_core:
|
||||
path: ../cw_core
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
build_runner: ^2.1.11
|
||||
mobx_codegen: ^2.0.7
|
||||
hive_generator: ^1.1.3
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
|
||||
# To add assets to your package, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
#
|
||||
# For details regarding assets in packages, see
|
||||
# https://flutter.dev/assets-and-images/#from-packages
|
||||
#
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||
|
||||
# To add custom fonts to your package, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts in packages, see
|
||||
# https://flutter.dev/custom-fonts/#from-packages
|
12
cw_ethereum/test/cw_ethereum_test.dart
Normal file
12
cw_ethereum/test/cw_ethereum_test.dart
Normal file
|
@ -0,0 +1,12 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:cw_ethereum/cw_ethereum.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);
|
||||
});
|
||||
}
|
|
@ -254,6 +254,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
|
|||
await haven_wallet.store();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> renameWalletFiles(String newWalletName) async {
|
||||
final currentWalletPath = await pathForWallet(name: name, type: type);
|
||||
final currentCacheFile = File(currentWalletPath);
|
||||
|
|
|
@ -14,18 +14,18 @@ class MoneroTransactionInfo extends TransactionInfo {
|
|||
MoneroTransactionInfo.fromMap(Map<String, Object?> map)
|
||||
: id = (map['hash'] ?? '') as String,
|
||||
height = (map['height'] ?? 0) as int,
|
||||
direction =
|
||||
parseTransactionDirectionFromNumber(map['direction'] as String) ??
|
||||
TransactionDirection.incoming,
|
||||
direction = map['direction'] != null
|
||||
? parseTransactionDirectionFromNumber(map['direction'] as String)
|
||||
: TransactionDirection.incoming,
|
||||
date = DateTime.fromMillisecondsSinceEpoch(
|
||||
(int.parse(map['timestamp'] as String) ?? 0) * 1000),
|
||||
(int.tryParse(map['timestamp'] as String? ?? '') ?? 0) * 1000),
|
||||
isPending = parseBoolFromString(map['isPending'] as String),
|
||||
amount = map['amount'] as int,
|
||||
accountIndex = int.parse(map['accountIndex'] as String),
|
||||
addressIndex = map['addressIndex'] as int,
|
||||
confirmations = map['confirmations'] as int,
|
||||
key = getTxKey((map['hash'] ?? '') as String),
|
||||
fee = map['fee'] as int ?? 0 {
|
||||
fee = map['fee'] as int? ?? 0 {
|
||||
additionalInfo = <String, dynamic>{
|
||||
'key': key,
|
||||
'accountIndex': accountIndex,
|
||||
|
@ -36,8 +36,7 @@ class MoneroTransactionInfo extends TransactionInfo {
|
|||
MoneroTransactionInfo.fromRow(TransactionInfoRow row)
|
||||
: id = row.getHash(),
|
||||
height = row.blockHeight,
|
||||
direction = parseTransactionDirectionFromInt(row.direction) ??
|
||||
TransactionDirection.incoming,
|
||||
direction = parseTransactionDirectionFromInt(row.direction),
|
||||
date = DateTime.fromMillisecondsSinceEpoch(row.getDatetime() * 1000),
|
||||
isPending = row.isPending != 0,
|
||||
amount = row.getAmount(),
|
||||
|
|
|
@ -269,6 +269,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
|
|||
await monero_wallet.store();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> renameWalletFiles(String newWalletName) async {
|
||||
final currentWalletDirPath = await pathForWalletDir(name: name, type: type);
|
||||
|
||||
|
|
|
@ -6,9 +6,9 @@ The following are the system requirements to build CakeWallet for your Android d
|
|||
|
||||
```
|
||||
Ubuntu >= 16.04
|
||||
Android SDK 28
|
||||
Android SDK 29 or higher (better to have the latest one 33)
|
||||
Android NDK 17c
|
||||
Flutter 2 or above
|
||||
Flutter 3.7.x
|
||||
```
|
||||
|
||||
## Building CakeWallet on Android
|
||||
|
@ -55,7 +55,7 @@ You may download and install the latest version of Android Studio [here](https:/
|
|||
|
||||
### 3. Installing Flutter
|
||||
|
||||
Need to install flutter with version `3.x.x`. For this please check section [Install Flutter manually](https://docs.flutter.dev/get-started/install/linux#install-flutter-manually).
|
||||
Need to install flutter with version `3.7.x`. For this please check section [Install Flutter manually](https://docs.flutter.dev/get-started/install/linux#install-flutter-manually).
|
||||
|
||||
### 4. Verify Installations
|
||||
|
||||
|
@ -66,9 +66,9 @@ Verify that the Android toolchain, Flutter, and Android Studio have been correct
|
|||
The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding.
|
||||
```
|
||||
Doctor summary (to see all details, run flutter doctor -v):
|
||||
[✓] Flutter (Channel stable, 3.x.x, on Linux, locale en_US.UTF-8)
|
||||
[✓] Android toolchain - develop for Android devices (Android SDK version 28)
|
||||
[✓] Android Studio (version 4.0)
|
||||
[✓] Flutter (Channel stable, 3.7.x, on Linux, locale en_US.UTF-8)
|
||||
[✓] Android toolchain - develop for Android devices (Android SDK version 29 or higher)
|
||||
[✓] Android Studio (version 4.0 or higher)
|
||||
```
|
||||
|
||||
### 5. Generate a secure keystore for Android
|
||||
|
|
|
@ -80,7 +80,7 @@ class CWBitcoin extends Bitcoin {
|
|||
isParsedAddress: out.isParsedAddress,
|
||||
formattedCryptoAmount: out.formattedCryptoAmount))
|
||||
.toList(),
|
||||
priority: priority != null ? priority as BitcoinTransactionPriority : null,
|
||||
priority: priority as BitcoinTransactionPriority,
|
||||
feeRate: feeRate);
|
||||
|
||||
@override
|
||||
|
|
|
@ -2,6 +2,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:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/erc20_token.dart';
|
||||
|
||||
class AddressValidator extends TextValidator {
|
||||
AddressValidator({required CryptoCurrency type})
|
||||
|
@ -14,6 +15,9 @@ class AddressValidator extends TextValidator {
|
|||
length: getLength(type));
|
||||
|
||||
static String getPattern(CryptoCurrency type) {
|
||||
if (type is Erc20Token) {
|
||||
return '0x[0-9a-zA-Z]';
|
||||
}
|
||||
switch (type) {
|
||||
case CryptoCurrency.xmr:
|
||||
return '^4[0-9a-zA-Z]{94}\$|^8[0-9a-zA-Z]{94}\$|^[0-9a-zA-Z]{106}\$';
|
||||
|
@ -56,6 +60,7 @@ class AddressValidator extends TextValidator {
|
|||
case CryptoCurrency.zrx:
|
||||
case CryptoCurrency.dydx:
|
||||
case CryptoCurrency.steth:
|
||||
case CryptoCurrency.shib:
|
||||
return '0x[0-9a-zA-Z]';
|
||||
case CryptoCurrency.xrp:
|
||||
return '^[0-9a-zA-Z]{34}\$|^X[0-9a-zA-Z]{46}\$';
|
||||
|
@ -116,17 +121,14 @@ class AddressValidator extends TextValidator {
|
|||
}
|
||||
|
||||
static List<int>? getLength(CryptoCurrency type) {
|
||||
if (type is Erc20Token) {
|
||||
return [42];
|
||||
}
|
||||
switch (type) {
|
||||
case CryptoCurrency.xmr:
|
||||
return null;
|
||||
case CryptoCurrency.ada:
|
||||
return null;
|
||||
case CryptoCurrency.avaxc:
|
||||
return [42];
|
||||
case CryptoCurrency.bch:
|
||||
return [42];
|
||||
case CryptoCurrency.bnb:
|
||||
return [42];
|
||||
case CryptoCurrency.btc:
|
||||
return null;
|
||||
case CryptoCurrency.dash:
|
||||
|
@ -166,6 +168,10 @@ class AddressValidator extends TextValidator {
|
|||
case CryptoCurrency.zrx:
|
||||
case CryptoCurrency.dydx:
|
||||
case CryptoCurrency.steth:
|
||||
case CryptoCurrency.shib:
|
||||
case CryptoCurrency.avaxc:
|
||||
case CryptoCurrency.bch:
|
||||
case CryptoCurrency.bnb:
|
||||
return [42];
|
||||
case CryptoCurrency.ltc:
|
||||
return [34, 43, 63];
|
||||
|
@ -203,11 +209,8 @@ class AddressValidator extends TextValidator {
|
|||
case CryptoCurrency.xusd:
|
||||
return [98, 99, 106];
|
||||
case CryptoCurrency.btt:
|
||||
return [34];
|
||||
case CryptoCurrency.bttc:
|
||||
return [34];
|
||||
case CryptoCurrency.doge:
|
||||
return [34];
|
||||
case CryptoCurrency.firo:
|
||||
return [34];
|
||||
case CryptoCurrency.hbar:
|
||||
|
@ -258,6 +261,8 @@ class AddressValidator extends TextValidator {
|
|||
return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)'
|
||||
'|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)'
|
||||
'|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)';
|
||||
case CryptoCurrency.eth:
|
||||
return '0x[0-9a-zA-Z]{42}';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -240,6 +240,9 @@ class BackupService {
|
|||
data[PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets] as bool?;
|
||||
final shouldRequireTOTP2FAForAllSecurityAndBackupSettings =
|
||||
data[PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings] as bool?;
|
||||
final sortBalanceTokensBy = data[PreferencesKey.sortBalanceBy] as int?;
|
||||
final pinNativeTokenAtTop = data[PreferencesKey.pinNativeTokenAtTop] as bool?;
|
||||
final useEtherscan = data[PreferencesKey.useEtherscan] as bool?;
|
||||
|
||||
await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName);
|
||||
|
||||
|
@ -349,6 +352,15 @@ class BackupService {
|
|||
PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
|
||||
shouldRequireTOTP2FAForAllSecurityAndBackupSettings);
|
||||
|
||||
if (sortBalanceTokensBy != null)
|
||||
await _sharedPreferences.setInt(PreferencesKey.sortBalanceBy, sortBalanceTokensBy);
|
||||
|
||||
if (pinNativeTokenAtTop != null)
|
||||
await _sharedPreferences.setBool(PreferencesKey.pinNativeTokenAtTop, pinNativeTokenAtTop);
|
||||
|
||||
if (useEtherscan != null)
|
||||
await _sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan);
|
||||
|
||||
await preferencesFile.delete();
|
||||
}
|
||||
|
||||
|
@ -492,6 +504,12 @@ class BackupService {
|
|||
_sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets),
|
||||
PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings: _sharedPreferences
|
||||
.getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings),
|
||||
PreferencesKey.sortBalanceBy:
|
||||
_sharedPreferences.getInt(PreferencesKey.sortBalanceBy),
|
||||
PreferencesKey.pinNativeTokenAtTop:
|
||||
_sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop),
|
||||
PreferencesKey.useEtherscan:
|
||||
_sharedPreferences.getBool(PreferencesKey.useEtherscan),
|
||||
};
|
||||
|
||||
return json.encode(preferences);
|
||||
|
|
|
@ -5,20 +5,19 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:http/http.dart';
|
||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||
|
||||
|
||||
const _fiatApiClearNetAuthority = 'fiat-api.cakewallet.com';
|
||||
const _fiatApiOnionAuthority = 'n4z7bdcmwk2oyddxvzaap3x2peqcplh3pzdy7tpkk5ejz5n4mhfvoxqd.onion';
|
||||
const _fiatApiPath = '/v2/rates';
|
||||
|
||||
Future<double> _fetchPrice(Map<String, dynamic> args) async {
|
||||
final crypto = args['crypto'] as CryptoCurrency;
|
||||
final fiat = args['fiat'] as FiatCurrency;
|
||||
final crypto = args['crypto'] as String;
|
||||
final fiat = args['fiat'] as String;
|
||||
final torOnly = args['torOnly'] as bool;
|
||||
|
||||
final Map<String, String> queryParams = {
|
||||
'interval_count': '1',
|
||||
'base': crypto.toString(),
|
||||
'quote': fiat.toString(),
|
||||
'base': crypto,
|
||||
'quote': fiat,
|
||||
'key': secrets.fiatApiKey,
|
||||
};
|
||||
|
||||
|
@ -52,7 +51,11 @@ Future<double> _fetchPrice(Map<String, dynamic> args) async {
|
|||
}
|
||||
|
||||
Future<double> _fetchPriceAsync(CryptoCurrency crypto, FiatCurrency fiat, bool torOnly) async =>
|
||||
compute(_fetchPrice, {'fiat': fiat, 'crypto': crypto, 'torOnly': torOnly});
|
||||
compute(_fetchPrice, {
|
||||
'fiat': fiat.toString(),
|
||||
'crypto': crypto.toString(),
|
||||
'torOnly': torOnly,
|
||||
});
|
||||
|
||||
class FiatConversionService {
|
||||
static Future<double> fetchPrice({
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/ethereum/ethereum.dart';
|
||||
import 'package:cake_wallet/haven/haven.dart';
|
||||
import 'package:cake_wallet/core/validator.dart';
|
||||
import 'package:cake_wallet/entities/mnemonic_item.dart';
|
||||
|
@ -25,6 +26,8 @@ class SeedValidator extends Validator<MnemonicItem> {
|
|||
return monero!.getMoneroWordList(language);
|
||||
case WalletType.haven:
|
||||
return haven!.getMoneroWordList(language);
|
||||
case WalletType.ethereum:
|
||||
return ethereum!.getEthereumWordList(language);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
|
33
lib/di.dart
33
lib/di.dart
|
@ -7,6 +7,7 @@ import 'package:cake_wallet/core/yat_service.dart';
|
|||
import 'package:cake_wallet/entities/exchange_api_mode.dart';
|
||||
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
|
||||
import 'package:cake_wallet/entities/receive_page_option.dart';
|
||||
import 'package:cake_wallet/ethereum/ethereum.dart';
|
||||
import 'package:cake_wallet/ionia/ionia_anypay.dart';
|
||||
import 'package:cake_wallet/ionia/ionia_gift_card.dart';
|
||||
import 'package:cake_wallet/ionia/ionia_tip.dart';
|
||||
|
@ -16,6 +17,8 @@ import 'package:cake_wallet/src/screens/buy/webview_page.dart';
|
|||
import 'package:cake_wallet/src/screens/dashboard/desktop_dashboard_page.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart';
|
||||
import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart';
|
||||
import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart';
|
||||
|
@ -40,6 +43,7 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
|||
import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/anonpay_details_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/market_place_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart';
|
||||
|
@ -70,6 +74,7 @@ import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart
|
|||
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart';
|
||||
import 'package:cw_core/erc20_token.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
import 'package:cake_wallet/core/backup_service.dart';
|
||||
import 'package:cw_core/wallet_service.dart';
|
||||
|
@ -239,9 +244,9 @@ Future setup({
|
|||
getIt.registerSingletonAsync<SharedPreferences>(() => SharedPreferences.getInstance());
|
||||
}
|
||||
|
||||
final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty ?? false) &&
|
||||
(secrets.wyreApiKey.isNotEmpty ?? false) &&
|
||||
(secrets.wyreAccountId.isNotEmpty ?? false);
|
||||
final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty) &&
|
||||
(secrets.wyreApiKey.isNotEmpty) &&
|
||||
(secrets.wyreAccountId.isNotEmpty);
|
||||
|
||||
final settingsStore = await SettingsStoreBase.load(
|
||||
nodeSource: _nodeSource,
|
||||
|
@ -638,7 +643,7 @@ Future setup({
|
|||
});
|
||||
|
||||
getIt.registerFactory(() {
|
||||
return PrivacySettingsViewModel(getIt.get<SettingsStore>());
|
||||
return PrivacySettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!);
|
||||
});
|
||||
|
||||
getIt.registerFactory(() {
|
||||
|
@ -745,6 +750,8 @@ Future setup({
|
|||
return bitcoin!.createBitcoinWalletService(_walletInfoSource, _unspentCoinsInfoSource!);
|
||||
case WalletType.litecoin:
|
||||
return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource!);
|
||||
case WalletType.ethereum:
|
||||
return ethereum!.createEthereumWalletService(_walletInfoSource);
|
||||
default:
|
||||
throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService');
|
||||
}
|
||||
|
@ -787,8 +794,8 @@ Future setup({
|
|||
transactionDetailsViewModel:
|
||||
getIt.get<TransactionDetailsViewModel>(param1: transactionInfo)));
|
||||
|
||||
getIt.registerFactoryParam<NewWalletTypePage, void Function(BuildContext, WalletType), void>(
|
||||
(param1, _) => NewWalletTypePage(onTypeSelected: param1));
|
||||
getIt.registerFactoryParam<NewWalletTypePage, void Function(BuildContext, WalletType), bool?>(
|
||||
(param1, isCreate) => NewWalletTypePage(onTypeSelected: param1, isCreate: isCreate ?? true));
|
||||
|
||||
getIt.registerFactoryParam<PreSeedPage, WalletType, void>(
|
||||
(WalletType type, _) => PreSeedPage(type));
|
||||
|
@ -1034,5 +1041,19 @@ Future setup({
|
|||
getIt.registerFactoryParam<AdvancedPrivacySettingsViewModel, WalletType, void>(
|
||||
(type, _) => AdvancedPrivacySettingsViewModel(type, getIt.get<SettingsStore>()));
|
||||
|
||||
getIt.registerFactoryParam<HomeSettingsPage, BalanceViewModel, void>((balanceViewModel, _) =>
|
||||
HomeSettingsPage(getIt.get<HomeSettingsViewModel>(param1: balanceViewModel)));
|
||||
|
||||
getIt.registerFactoryParam<HomeSettingsViewModel, BalanceViewModel, void>(
|
||||
(balanceViewModel, _) => HomeSettingsViewModel(getIt.get<SettingsStore>(), balanceViewModel));
|
||||
|
||||
getIt.registerFactoryParam<EditTokenPage, HomeSettingsViewModel, Map<String, dynamic>>(
|
||||
(homeSettingsViewModel, arguments) => EditTokenPage(
|
||||
homeSettingsViewModel: homeSettingsViewModel,
|
||||
erc20token: arguments['token'] as Erc20Token?,
|
||||
initialContractAddress: arguments['contractAddress'] as String?,
|
||||
),
|
||||
);
|
||||
|
||||
_isSetupFinished = true;
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081';
|
|||
const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002';
|
||||
const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002';
|
||||
const havenDefaultNodeUri = 'nodes.havenprotocol.org:443';
|
||||
const ethereumDefaultNodeUri = 'ethereum.publicnode.com';
|
||||
|
||||
Future defaultSettingsMigration(
|
||||
{required int version,
|
||||
|
@ -157,6 +158,12 @@ Future defaultSettingsMigration(
|
|||
case 20:
|
||||
await migrateExchangeStatus(sharedPreferences);
|
||||
break;
|
||||
case 21:
|
||||
await addEthereumNodeList(nodes: nodes);
|
||||
await changeEthereumCurrentNodeToDefault(
|
||||
sharedPreferences: sharedPreferences, nodes: nodes);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -242,6 +249,12 @@ Node? getHavenDefaultNode({required Box<Node> nodes}) {
|
|||
?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.haven);
|
||||
}
|
||||
|
||||
Node? getEthereumDefaultNode({required Box<Node> nodes}) {
|
||||
return nodes.values.firstWhereOrNull(
|
||||
(Node node) => node.uriRaw == ethereumDefaultNodeUri)
|
||||
?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.ethereum);
|
||||
}
|
||||
|
||||
Node getMoneroDefaultNode({required Box<Node> nodes}) {
|
||||
final timeZone = DateTime.now().timeZoneOffset.inHours;
|
||||
var nodeUri = '';
|
||||
|
@ -438,6 +451,8 @@ Future<void> checkCurrentNodes(
|
|||
.getInt(PreferencesKey.currentLitecoinElectrumSererIdKey);
|
||||
final currentHavenNodeId = sharedPreferences
|
||||
.getInt(PreferencesKey.currentHavenNodeIdKey);
|
||||
final currentEthereumNodeId = sharedPreferences
|
||||
.getInt(PreferencesKey.currentEthereumNodeIdKey);
|
||||
final currentMoneroNode = nodeSource.values.firstWhereOrNull(
|
||||
(node) => node.key == currentMoneroNodeId);
|
||||
final currentBitcoinElectrumServer = nodeSource.values.firstWhereOrNull(
|
||||
|
@ -446,6 +461,8 @@ Future<void> checkCurrentNodes(
|
|||
(node) => node.key == currentLitecoinElectrumSeverId);
|
||||
final currentHavenNodeServer = nodeSource.values.firstWhereOrNull(
|
||||
(node) => node.key == currentHavenNodeId);
|
||||
final currentEthereumNodeServer = nodeSource.values.firstWhereOrNull(
|
||||
(node) => node.key == currentEthereumNodeId);
|
||||
|
||||
if (currentMoneroNode == null) {
|
||||
final newCakeWalletNode =
|
||||
|
@ -479,6 +496,13 @@ Future<void> checkCurrentNodes(
|
|||
await sharedPreferences.setInt(
|
||||
PreferencesKey.currentHavenNodeIdKey, node.key as int);
|
||||
}
|
||||
|
||||
if (currentEthereumNodeServer == null) {
|
||||
final node = Node(uri: ethereumDefaultNodeUri, type: WalletType.ethereum);
|
||||
await nodeSource.add(node);
|
||||
await sharedPreferences.setInt(
|
||||
PreferencesKey.currentEthereumNodeIdKey, node.key as int);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> resetBitcoinElectrumServer(
|
||||
|
@ -527,3 +551,21 @@ Future<void> migrateExchangeStatus(SharedPreferences sharedPreferences) async {
|
|||
|
||||
await sharedPreferences.remove(PreferencesKey.disableExchangeKey);
|
||||
}
|
||||
|
||||
Future<void> addEthereumNodeList({required Box<Node> nodes}) async {
|
||||
final nodeList = await loadDefaultEthereumNodes();
|
||||
for (var node in nodeList) {
|
||||
if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) {
|
||||
await nodes.add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> changeEthereumCurrentNodeToDefault(
|
||||
{required SharedPreferences sharedPreferences,
|
||||
required Box<Node> nodes}) async {
|
||||
final node = getEthereumDefaultNode(nodes: nodes);
|
||||
final nodeId = node?.key as int? ?? 0;
|
||||
|
||||
await sharedPreferences.setInt(PreferencesKey.currentEthereumNodeIdKey, nodeId);
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ class MainActions {
|
|||
switch (walletType) {
|
||||
case WalletType.bitcoin:
|
||||
case WalletType.litecoin:
|
||||
case WalletType.ethereum:
|
||||
if (viewModel.isEnabledBuyAction) {
|
||||
final uri = getIt.get<OnRamperBuyProvider>().requestUrl();
|
||||
if (DeviceInfo.instance.isMobile) {
|
||||
|
@ -116,6 +117,7 @@ class MainActions {
|
|||
switch (walletType) {
|
||||
case WalletType.bitcoin:
|
||||
case WalletType.litecoin:
|
||||
case WalletType.ethereum:
|
||||
if (viewModel.isEnabledSellAction) {
|
||||
final moonPaySellProvider = MoonPaySellProvider();
|
||||
final uri = await moonPaySellProvider.requestUrl(
|
||||
|
|
|
@ -70,6 +70,22 @@ Future<List<Node>> loadDefaultHavenNodes() async {
|
|||
return nodes;
|
||||
}
|
||||
|
||||
Future<List<Node>> loadDefaultEthereumNodes() async {
|
||||
final nodesRaw = await rootBundle.loadString('assets/ethereum_server_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.ethereum;
|
||||
nodes.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
Future resetToDefault(Box<Node> nodeSource) async {
|
||||
final moneroNodes = await loadDefaultNodes();
|
||||
final bitcoinElectrumServerList = await loadBitcoinElectrumServerList();
|
||||
|
|
|
@ -5,6 +5,7 @@ class PreferencesKey {
|
|||
static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc';
|
||||
static const currentLitecoinElectrumSererIdKey = 'current_node_id_ltc';
|
||||
static const currentHavenNodeIdKey = 'current_node_id_xhv';
|
||||
static const currentEthereumNodeIdKey = 'current_node_id_eth';
|
||||
static const currentFiatCurrencyKey = 'current_fiat_currency';
|
||||
static const currentTransactionPriorityKeyLegacy = 'current_fee_priority';
|
||||
static const currentBalanceDisplayModeKey = 'current_balance_display_mode';
|
||||
|
@ -31,6 +32,7 @@ class PreferencesKey {
|
|||
static const bitcoinTransactionPriority = 'current_fee_priority_bitcoin';
|
||||
static const havenTransactionPriority = 'current_fee_priority_haven';
|
||||
static const litecoinTransactionPriority = 'current_fee_priority_litecoin';
|
||||
static const ethereumTransactionPriority = 'current_fee_priority_ethereum';
|
||||
static const shouldShowReceiveWarning = 'should_show_receive_warning';
|
||||
static const shouldShowYatPopup = 'should_show_yat_popup';
|
||||
static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1';
|
||||
|
@ -38,6 +40,9 @@ class PreferencesKey {
|
|||
static const lastAuthTimeMilliseconds = 'last_auth_time_milliseconds';
|
||||
static const lastPopupDate = 'last_popup_date';
|
||||
static const lastAppReviewDate = 'last_app_review_date';
|
||||
static const sortBalanceBy = 'sort_balance_by';
|
||||
static const pinNativeTokenAtTop = 'pin_native_token_at_top';
|
||||
static const useEtherscan = 'use_etherscan';
|
||||
|
||||
static String moneroWalletUpdateV1Key(String name) =>
|
||||
'${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/ethereum/ethereum.dart';
|
||||
import 'package:cake_wallet/haven/haven.dart';
|
||||
import 'package:cake_wallet/monero/monero.dart';
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
|
@ -14,6 +15,8 @@ List<TransactionPriority> priorityForWalletType(WalletType type) {
|
|||
return bitcoin!.getLitecoinTransactionPriorities();
|
||||
case WalletType.haven:
|
||||
return haven!.getTransactionPriorities();
|
||||
case WalletType.ethereum:
|
||||
return ethereum!.getTransactionPriorities();
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
|
19
lib/entities/sort_balance_types.dart
Normal file
19
lib/entities/sort_balance_types.dart
Normal file
|
@ -0,0 +1,19 @@
|
|||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
|
||||
enum SortBalanceBy {
|
||||
FiatBalance,
|
||||
GrossBalance,
|
||||
Alphabetical;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
switch (this) {
|
||||
case SortBalanceBy.FiatBalance:
|
||||
return S.current.fiat_balance;
|
||||
case SortBalanceBy.GrossBalance:
|
||||
return S.current.gross_balance;
|
||||
case SortBalanceBy.Alphabetical:
|
||||
return S.current.alphabetical;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -55,5 +55,5 @@ class Template extends HiveObject {
|
|||
|
||||
String get amount => amountRaw ?? '';
|
||||
|
||||
List<Template>? get additionalRecipients => additionalRecipientsRaw ?? null;
|
||||
List<Template>? get additionalRecipients => additionalRecipientsRaw;
|
||||
}
|
||||
|
|
126
lib/ethereum/cw_ethereum.dart
Normal file
126
lib/ethereum/cw_ethereum.dart
Normal file
|
@ -0,0 +1,126 @@
|
|||
part of 'ethereum.dart';
|
||||
|
||||
class CWEthereum extends Ethereum {
|
||||
@override
|
||||
List<String> getEthereumWordList(String language) => EthereumMnemonics.englishWordlist;
|
||||
|
||||
WalletService createEthereumWalletService(Box<WalletInfo> walletInfoSource) =>
|
||||
EthereumWalletService(walletInfoSource);
|
||||
|
||||
@override
|
||||
WalletCredentials createEthereumNewWalletCredentials({
|
||||
required String name,
|
||||
WalletInfo? walletInfo,
|
||||
}) =>
|
||||
EthereumNewWalletCredentials(name: name, walletInfo: walletInfo);
|
||||
|
||||
@override
|
||||
WalletCredentials createEthereumRestoreWalletFromSeedCredentials({
|
||||
required String name,
|
||||
required String mnemonic,
|
||||
required String password,
|
||||
}) =>
|
||||
EthereumRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic);
|
||||
|
||||
@override
|
||||
String getAddress(WalletBase wallet) => (wallet as EthereumWallet).walletAddresses.address;
|
||||
|
||||
@override
|
||||
TransactionPriority getDefaultTransactionPriority() => EthereumTransactionPriority.medium;
|
||||
|
||||
@override
|
||||
List<TransactionPriority> getTransactionPriorities() => EthereumTransactionPriority.all;
|
||||
|
||||
@override
|
||||
TransactionPriority deserializeEthereumTransactionPriority(int raw) =>
|
||||
EthereumTransactionPriority.deserialize(raw: raw);
|
||||
|
||||
Object createEthereumTransactionCredentials(
|
||||
List<Output> outputs, {
|
||||
required TransactionPriority priority,
|
||||
required CryptoCurrency currency,
|
||||
int? feeRate,
|
||||
}) =>
|
||||
EthereumTransactionCredentials(
|
||||
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(),
|
||||
priority: priority as EthereumTransactionPriority,
|
||||
currency: currency,
|
||||
feeRate: feeRate,
|
||||
);
|
||||
|
||||
Object createEthereumTransactionCredentialsRaw(
|
||||
List<OutputInfo> outputs, {
|
||||
TransactionPriority? priority,
|
||||
required CryptoCurrency currency,
|
||||
required int feeRate,
|
||||
}) =>
|
||||
EthereumTransactionCredentials(
|
||||
outputs,
|
||||
priority: priority as EthereumTransactionPriority?,
|
||||
currency: currency,
|
||||
feeRate: feeRate,
|
||||
);
|
||||
|
||||
@override
|
||||
int formatterEthereumParseAmount(String amount) => EthereumFormatter.parseEthereumAmount(amount);
|
||||
|
||||
@override
|
||||
double formatterEthereumAmountToDouble(
|
||||
{TransactionInfo? transaction, BigInt? amount, int exponent = 18}) {
|
||||
assert(transaction != null || amount != null);
|
||||
|
||||
if (transaction != null) {
|
||||
transaction as EthereumTransactionInfo;
|
||||
return transaction.ethAmount / BigInt.from(10).pow(transaction.exponent);
|
||||
} else {
|
||||
return (amount!) / BigInt.from(10).pow(exponent);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
List<Erc20Token> getERC20Currencies(WalletBase wallet) {
|
||||
final ethereumWallet = wallet as EthereumWallet;
|
||||
return ethereumWallet.erc20Currencies;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addErc20Token(WalletBase wallet, Erc20Token token) async =>
|
||||
await (wallet as EthereumWallet).addErc20Token(token);
|
||||
|
||||
@override
|
||||
Future<void> deleteErc20Token(WalletBase wallet, Erc20Token token) async =>
|
||||
await (wallet as EthereumWallet).deleteErc20Token(token);
|
||||
|
||||
@override
|
||||
Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress) async {
|
||||
final ethereumWallet = wallet as EthereumWallet;
|
||||
return await ethereumWallet.getErc20Token(contractAddress);
|
||||
}
|
||||
|
||||
@override
|
||||
CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) {
|
||||
transaction as EthereumTransactionInfo;
|
||||
if (transaction.tokenSymbol == CryptoCurrency.eth.title) {
|
||||
return CryptoCurrency.eth;
|
||||
}
|
||||
|
||||
wallet as EthereumWallet;
|
||||
return wallet.erc20Currencies
|
||||
.firstWhere((element) => transaction.tokenSymbol == element.symbol);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateEtherscanUsageState(WalletBase wallet, bool isEnabled) {
|
||||
(wallet as EthereumWallet).updateEtherscanUsageState(isEnabled);
|
||||
}
|
||||
}
|
|
@ -141,7 +141,7 @@ Future<void> main() async {
|
|||
transactionDescriptions: transactionDescriptions,
|
||||
secureStorage: secureStorage,
|
||||
anonpayInvoiceInfo: anonpayInvoiceInfo,
|
||||
initialMigrationVersion: 19);
|
||||
initialMigrationVersion: 21);
|
||||
runApp(App());
|
||||
}, (error, stackTrace) async {
|
||||
ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stackTrace));
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
import 'package:cake_wallet/core/fiat_conversion_service.dart';
|
||||
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/store/app_store.dart';
|
||||
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
|
@ -31,6 +32,20 @@ Future<void> startFiatRateUpdate(
|
|||
fiat: settingsStore.fiatCurrency,
|
||||
torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly);
|
||||
}
|
||||
|
||||
if (appStore.wallet!.type == WalletType.ethereum) {
|
||||
final currencies =
|
||||
ethereum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled);
|
||||
|
||||
for (final currency in currencies) {
|
||||
() async {
|
||||
fiatConversionStore.prices[currency] = await FiatConversionService.fetchPrice(
|
||||
crypto: currency,
|
||||
fiat: settingsStore.fiatCurrency,
|
||||
torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly);
|
||||
}.call();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:cake_wallet/entities/fiat_api_mode.dart';
|
||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||
import 'package:cake_wallet/entities/update_haven_rate.dart';
|
||||
import 'package:cake_wallet/ethereum/ethereum.dart';
|
||||
import 'package:cw_core/transaction_history.dart';
|
||||
import 'package:cw_core/balance.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
|
@ -97,6 +97,20 @@ void startCurrentWalletChangeReaction(AppStore appStore,
|
|||
crypto: wallet.currency,
|
||||
fiat: settingsStore.fiatCurrency,
|
||||
torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly);
|
||||
|
||||
if (wallet.type == WalletType.ethereum) {
|
||||
final currencies =
|
||||
ethereum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled);
|
||||
|
||||
for (final currency in currencies) {
|
||||
() async {
|
||||
fiatConversionStore.prices[currency] = await FiatConversionService.fetchPrice(
|
||||
crypto: currency,
|
||||
fiat: settingsStore.fiatCurrency,
|
||||
torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly);
|
||||
}.call();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart';
|
|||
import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart';
|
||||
import 'package:cake_wallet/src/screens/buy/webview_page.dart';
|
||||
import 'package:cake_wallet/src/screens/buy/pre_order_page.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart';
|
||||
import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart';
|
||||
import 'package:cake_wallet/src/screens/restore/sweeping_wallet_page.dart';
|
||||
import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart';
|
||||
import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart';
|
||||
|
@ -584,6 +586,25 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
|||
case Routes.modify2FAPage:
|
||||
return MaterialPageRoute<void>(builder: (_) => getIt.get<Modify2FAPage>());
|
||||
|
||||
case Routes.homeSettings:
|
||||
return CupertinoPageRoute<void>(
|
||||
builder: (_) => getIt.get<HomeSettingsPage>(param1: settings.arguments),
|
||||
);
|
||||
|
||||
case Routes.editToken:
|
||||
final args = settings.arguments as Map<String, dynamic>;
|
||||
|
||||
return CupertinoPageRoute<void>(
|
||||
settings: RouteSettings(name: Routes.editToken),
|
||||
builder: (_) => getIt.get<EditTokenPage>(
|
||||
param1: args['homeSettingsViewModel'],
|
||||
param2: {
|
||||
'token': args['token'],
|
||||
'contractAddress': args['contractAddress'],
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
default:
|
||||
return MaterialPageRoute<void>(
|
||||
builder: (_) => Scaffold(
|
||||
|
|
|
@ -88,4 +88,6 @@ class Routes {
|
|||
static const setup_2faQRPage = '/setup_2fa_qr_page';
|
||||
static const totpAuthCodePage = '/totp_auth_code_page';
|
||||
static const modify2FAPage = '/modify_2fa_page';
|
||||
static const homeSettings = '/home_settings';
|
||||
static const editToken = '/edit_token';
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
|
|||
final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24);
|
||||
final litecoinIcon = Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24);
|
||||
final havenIcon = Image.asset('assets/images/haven_logo.png', height: 24, width: 24);
|
||||
final ethereumIcon = Image.asset('assets/images/eth_icon.png', height: 24, width: 24);
|
||||
final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24);
|
||||
|
||||
Image _newWalletImage(BuildContext context) => Image.asset(
|
||||
|
@ -136,6 +137,8 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
|
|||
return litecoinIcon;
|
||||
case WalletType.haven:
|
||||
return havenIcon;
|
||||
case WalletType.ethereum:
|
||||
return ethereumIcon;
|
||||
default:
|
||||
return nonWalletTypeIcon;
|
||||
}
|
||||
|
|
309
lib/src/screens/dashboard/edit_token_page.dart
Normal file
309
lib/src/screens/dashboard/edit_token_page.dart
Normal file
|
@ -0,0 +1,309 @@
|
|||
import 'package:cake_wallet/core/address_validator.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/widgets/address_text_field.dart';
|
||||
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
|
||||
import 'package:cake_wallet/src/widgets/checkbox_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart';
|
||||
import 'package:cw_core/erc20_token.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class EditTokenPage extends BasePage {
|
||||
EditTokenPage({
|
||||
Key? key,
|
||||
required this.homeSettingsViewModel,
|
||||
this.erc20token,
|
||||
this.initialContractAddress,
|
||||
}) : assert(erc20token == null || initialContractAddress == null);
|
||||
|
||||
final HomeSettingsViewModel homeSettingsViewModel;
|
||||
final Erc20Token? erc20token;
|
||||
final String? initialContractAddress;
|
||||
|
||||
@override
|
||||
String? get title => S.current.edit_token;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
return EditTokenPageBody(
|
||||
homeSettingsViewModel: homeSettingsViewModel,
|
||||
erc20token: erc20token,
|
||||
initialContractAddress: initialContractAddress,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EditTokenPageBody extends StatefulWidget {
|
||||
const EditTokenPageBody({
|
||||
Key? key,
|
||||
required this.homeSettingsViewModel,
|
||||
this.erc20token,
|
||||
this.initialContractAddress,
|
||||
}) : super(key: key);
|
||||
|
||||
final HomeSettingsViewModel homeSettingsViewModel;
|
||||
final Erc20Token? erc20token;
|
||||
final String? initialContractAddress;
|
||||
|
||||
@override
|
||||
State<EditTokenPageBody> createState() => _EditTokenPageBodyState();
|
||||
}
|
||||
|
||||
class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
||||
final TextEditingController _contractAddressController = TextEditingController();
|
||||
final TextEditingController _tokenNameController = TextEditingController();
|
||||
final TextEditingController _tokenSymbolController = TextEditingController();
|
||||
final TextEditingController _tokenDecimalController = TextEditingController();
|
||||
|
||||
final FocusNode _contractAddressFocusNode = FocusNode();
|
||||
final FocusNode _tokenNameFocusNode = FocusNode();
|
||||
final FocusNode _tokenSymbolFocusNode = FocusNode();
|
||||
final FocusNode _tokenDecimalFocusNode = FocusNode();
|
||||
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
|
||||
bool _showDisclaimer = false;
|
||||
bool _disclaimerChecked = false;
|
||||
|
||||
@override
|
||||
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();
|
||||
}
|
||||
|
||||
if (widget.initialContractAddress != null) {
|
||||
_contractAddressController.text = widget.initialContractAddress!;
|
||||
_getTokenInfo();
|
||||
}
|
||||
|
||||
_contractAddressFocusNode.addListener(() {
|
||||
if (!_contractAddressFocusNode.hasFocus) {
|
||||
_getTokenInfo();
|
||||
}
|
||||
|
||||
final contractAddress = _contractAddressController.text;
|
||||
if (contractAddress.isNotEmpty && contractAddress != widget.erc20token?.contractAddress) {
|
||||
setState(() {
|
||||
_showDisclaimer = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: ScrollableWithBottomSection(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 25),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 16, horizontal: 28),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).accentTextTheme.bodySmall!.color!,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Image.asset('assets/images/restore_keys.png'),
|
||||
const SizedBox(width: 24),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).warning,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).primaryTextTheme.titleLarge!.color!,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 5),
|
||||
child: Text(
|
||||
S.of(context).add_token_warning,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: Theme.of(context).primaryTextTheme.labelSmall!.color!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 50),
|
||||
_tokenForm(),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
|
||||
bottomSection: Column(
|
||||
children: [
|
||||
if (_showDisclaimer) ...[
|
||||
CheckboxWidget(
|
||||
value: _disclaimerChecked,
|
||||
caption: S.of(context).add_token_disclaimer_check,
|
||||
onChanged: (value) {
|
||||
_disclaimerChecked = value;
|
||||
},
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
],
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
onPressed: () async {
|
||||
if (widget.erc20token != null) {
|
||||
await widget.homeSettingsViewModel.deleteErc20Token(widget.erc20token!);
|
||||
}
|
||||
Navigator.pop(context);
|
||||
},
|
||||
text: widget.erc20token != null ? S.of(context).delete : S.of(context).cancel,
|
||||
color: Colors.red,
|
||||
textColor: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
onPressed: () async {
|
||||
if (_formKey.currentState!.validate() &&
|
||||
(!_showDisclaimer || _disclaimerChecked)) {
|
||||
await widget.homeSettingsViewModel.addErc20Token(Erc20Token(
|
||||
name: _tokenNameController.text,
|
||||
symbol: _tokenSymbolController.text,
|
||||
contractAddress: _contractAddressController.text,
|
||||
decimal: int.parse(_tokenDecimalController.text),
|
||||
));
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
text: S.of(context).save,
|
||||
color: Theme.of(context).accentTextTheme.bodyLarge!.color!,
|
||||
textColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _getTokenInfo() async {
|
||||
if (_contractAddressController.text.isNotEmpty) {
|
||||
final token =
|
||||
await widget.homeSettingsViewModel.getErc20Token(_contractAddressController.text);
|
||||
|
||||
if (token != null) {
|
||||
if (_tokenNameController.text.isEmpty) _tokenNameController.text = token.name;
|
||||
if (_tokenSymbolController.text.isEmpty) _tokenSymbolController.text = token.symbol;
|
||||
if (_tokenDecimalController.text.isEmpty)
|
||||
_tokenDecimalController.text = token.decimal.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pasteText() async {
|
||||
final value = await Clipboard.getData('text/plain');
|
||||
|
||||
if (value?.text?.isNotEmpty ?? false) {
|
||||
_contractAddressController.text = value!.text!;
|
||||
|
||||
_getTokenInfo();
|
||||
setState(() {
|
||||
_showDisclaimer = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Widget _tokenForm() {
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
AddressTextField(
|
||||
controller: _contractAddressController,
|
||||
focusNode: _contractAddressFocusNode,
|
||||
placeholder: S.of(context).token_contract_address,
|
||||
options: [AddressTextFieldOption.paste],
|
||||
buttonColor: Theme.of(context).hintColor,
|
||||
validator: AddressValidator(type: widget.homeSettingsViewModel.nativeToken),
|
||||
onPushPasteButton: (_) {
|
||||
_pasteText();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
BaseTextFormField(
|
||||
controller: _tokenNameController,
|
||||
focusNode: _tokenNameFocusNode,
|
||||
onSubmit: (_) => FocusScope.of(context).requestFocus(_tokenSymbolFocusNode),
|
||||
textInputAction: TextInputAction.next,
|
||||
hintText: S.of(context).token_name,
|
||||
validator: (text) {
|
||||
if (text?.isNotEmpty ?? false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return S.of(context).field_required;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
BaseTextFormField(
|
||||
controller: _tokenSymbolController,
|
||||
focusNode: _tokenSymbolFocusNode,
|
||||
onSubmit: (_) => FocusScope.of(context).requestFocus(_tokenDecimalFocusNode),
|
||||
textInputAction: TextInputAction.next,
|
||||
hintText: S.of(context).token_symbol,
|
||||
validator: (text) {
|
||||
if (text?.isNotEmpty ?? false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return S.of(context).field_required;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
BaseTextFormField(
|
||||
controller: _tokenDecimalController,
|
||||
focusNode: _tokenDecimalFocusNode,
|
||||
textInputAction: TextInputAction.done,
|
||||
hintText: S.of(context).token_decimal,
|
||||
validator: (text) {
|
||||
if (text?.isEmpty ?? true) {
|
||||
return S.of(context).field_required;
|
||||
}
|
||||
if (int.tryParse(text!) == null) {
|
||||
return S.of(context).invalid_input;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
164
lib/src/screens/dashboard/home_settings_page.dart
Normal file
164
lib/src/screens/dashboard/home_settings_page.dart
Normal file
|
@ -0,0 +1,164 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:cake_wallet/core/address_validator.dart';
|
||||
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/screens/settings/widgets/settings_picker_cell.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
class HomeSettingsPage extends BasePage {
|
||||
HomeSettingsPage(this._homeSettingsViewModel);
|
||||
|
||||
final HomeSettingsViewModel _homeSettingsViewModel;
|
||||
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
@override
|
||||
String? get title => S.current.home_screen_settings;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Observer(
|
||||
builder: (_) => SettingsPickerCell<SortBalanceBy>(
|
||||
title: S.current.sort_by,
|
||||
items: SortBalanceBy.values,
|
||||
selectedItem: _homeSettingsViewModel.sortBalanceBy,
|
||||
onItemSelected: _homeSettingsViewModel.setSortBalanceBy,
|
||||
),
|
||||
),
|
||||
Divider(color: Theme.of(context).primaryTextTheme.bodySmall!.decorationColor!),
|
||||
Observer(
|
||||
builder: (_) => SettingsSwitcherCell(
|
||||
title: S.of(context).pin_at_top(_homeSettingsViewModel.nativeToken.title),
|
||||
value: _homeSettingsViewModel.pinNativeToken,
|
||||
onValueChange: (_, bool value) {
|
||||
_homeSettingsViewModel.setPinNativeToken(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
Divider(color: Theme.of(context).primaryTextTheme.bodySmall!.decorationColor!),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 16),
|
||||
child: TextFormField(
|
||||
controller: _searchController,
|
||||
style: TextStyle(color: Theme.of(context).primaryTextTheme.titleLarge!.color!),
|
||||
decoration: InputDecoration(
|
||||
hintText: S.of(context).search_add_token,
|
||||
prefixIcon: Image.asset("assets/images/search_icon.png"),
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).accentTextTheme.displaySmall!.color!,
|
||||
alignLabelWithHint: false,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
borderSide: const BorderSide(color: Colors.transparent),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
borderSide: const BorderSide(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
onChanged: (String text) => _homeSettingsViewModel.changeSearchText(text),
|
||||
),
|
||||
),
|
||||
),
|
||||
RawMaterialButton(
|
||||
onPressed: () async {
|
||||
Navigator.pushNamed(context, Routes.editToken, arguments: {
|
||||
'homeSettingsViewModel': _homeSettingsViewModel,
|
||||
if (AddressValidator(type: _homeSettingsViewModel.nativeToken)
|
||||
.isValid(_searchController.text))
|
||||
'contractAddress': _searchController.text,
|
||||
});
|
||||
},
|
||||
elevation: 0,
|
||||
fillColor: Theme.of(context).accentTextTheme.bodySmall!.color!,
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
color: Theme.of(context).primaryTextTheme.titleLarge!.color!,
|
||||
size: 22.0,
|
||||
),
|
||||
padding: EdgeInsets.all(12),
|
||||
shape: CircleBorder(),
|
||||
splashColor: Theme.of(context).accentTextTheme.bodySmall!.color!,
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16, left: 16, right: 16),
|
||||
child: Observer(
|
||||
builder: (_) => ListView.builder(
|
||||
itemCount: _homeSettingsViewModel.tokens.length,
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: 16),
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
final token = _homeSettingsViewModel.tokens.elementAt(index);
|
||||
|
||||
return SettingsSwitcherCell(
|
||||
title: "${token.name} "
|
||||
"(${token.symbol})",
|
||||
value: token.enabled,
|
||||
onValueChange: (_, bool value) {
|
||||
_homeSettingsViewModel.changeTokenAvailability(token, value);
|
||||
},
|
||||
onTap: (_) {
|
||||
Navigator.pushNamed(context, Routes.editToken, arguments: {
|
||||
'homeSettingsViewModel': _homeSettingsViewModel,
|
||||
'token': token,
|
||||
});
|
||||
},
|
||||
leading: token.iconPath != null
|
||||
? Container(
|
||||
child: Image.asset(
|
||||
token.iconPath!,
|
||||
height: 30.0,
|
||||
width: 30.0,
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
height: 30.0,
|
||||
width: 30.0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
token.symbol.substring(0, min(token.symbol.length, 2)),
|
||||
style: TextStyle(fontSize: 11),
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).accentTextTheme.bodySmall!.color!,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -63,15 +63,11 @@ class AddressPage extends BasePage {
|
|||
Widget? leading(BuildContext context) {
|
||||
final _backButton = Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.displayMedium!
|
||||
.backgroundColor!,
|
||||
color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!,
|
||||
size: 16,
|
||||
);
|
||||
final _closeButton = currentTheme.type == ThemeType.dark
|
||||
? closeButtonImageDarkTheme
|
||||
: closeButtonImage;
|
||||
final _closeButton =
|
||||
currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage;
|
||||
|
||||
bool isMobileView = ResponsiveLayoutUtil.instance.isMobile;
|
||||
|
||||
|
@ -82,13 +78,10 @@ class AddressPage extends BasePage {
|
|||
child: ButtonTheme(
|
||||
minWidth: double.minPositive,
|
||||
child: Semantics(
|
||||
label: !isMobileView
|
||||
? S.of(context).close
|
||||
: S.of(context).seed_alert_back,
|
||||
label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back,
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
overlayColor: MaterialStateColor.resolveWith(
|
||||
(states) => Colors.transparent),
|
||||
overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent),
|
||||
),
|
||||
onPressed: () => onClose(context),
|
||||
child: !isMobileView ? _closeButton : _backButton,
|
||||
|
@ -100,8 +93,7 @@ class AddressPage extends BasePage {
|
|||
}
|
||||
|
||||
@override
|
||||
Widget middle(BuildContext context) =>
|
||||
PresentReceiveOptionPicker(
|
||||
Widget middle(BuildContext context) => PresentReceiveOptionPicker(
|
||||
receiveOptionViewModel: receiveOptionViewModel,
|
||||
hasWhiteBackground: currentTheme.type == ThemeType.light,
|
||||
);
|
||||
|
@ -136,10 +128,7 @@ class AddressPage extends BasePage {
|
|||
icon: Icon(
|
||||
Icons.share,
|
||||
size: 20,
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.displayMedium!
|
||||
.backgroundColor!,
|
||||
color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -180,10 +169,7 @@ class AddressPage extends BasePage {
|
|||
tapOutsideToDismiss: true,
|
||||
config: KeyboardActionsConfig(
|
||||
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
|
||||
keyboardBarColor: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.bodyLarge!
|
||||
.backgroundColor!,
|
||||
keyboardBarColor: Theme.of(context).accentTextTheme.bodyLarge!.backgroundColor!,
|
||||
nextFocus: false,
|
||||
actions: [
|
||||
KeyboardActionsItem(
|
||||
|
@ -205,8 +191,8 @@ class AddressPage extends BasePage {
|
|||
isLight: dashboardViewModel.settingsStore.currentTheme.type ==
|
||||
ThemeType.light))),
|
||||
Observer(builder: (_) {
|
||||
return addressListViewModel.hasAddressList
|
||||
? GestureDetector(
|
||||
if (addressListViewModel.hasAddressList) {
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.of(context).pushNamed(Routes.receive),
|
||||
child: Container(
|
||||
height: 50,
|
||||
|
@ -215,15 +201,8 @@ class AddressPage extends BasePage {
|
|||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(25)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context)
|
||||
.textTheme!
|
||||
.titleMedium!
|
||||
.color!,
|
||||
width: 1),
|
||||
color: Theme.of(context)
|
||||
.textTheme!
|
||||
.titleLarge!
|
||||
.backgroundColor!),
|
||||
color: Theme.of(context).textTheme.titleMedium!.color!, width: 1),
|
||||
color: Theme.of(context).textTheme.titleLarge!.backgroundColor!),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
|
@ -237,30 +216,29 @@ class AddressPage extends BasePage {
|
|||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.displayMedium!
|
||||
.accentTextTheme.displayMedium!
|
||||
.backgroundColor!),
|
||||
)),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 14,
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.displayMedium!
|
||||
.backgroundColor!,
|
||||
color:
|
||||
Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: Text(S.of(context).electrum_address_disclaimer,
|
||||
);
|
||||
} else if (addressListViewModel.showElectrumAddressDisclaimer) {
|
||||
return Text(S.of(context).electrum_address_disclaimer,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.displaySmall!
|
||||
.backgroundColor!));
|
||||
color:
|
||||
Theme.of(context).accentTextTheme.displaySmall!.backgroundColor!));
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
})
|
||||
],
|
||||
),
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cake_wallet/themes/theme_base.dart';
|
||||
import 'package:cake_wallet/utils/feature_flag.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
|
@ -25,26 +25,50 @@ class BalancePage extends StatelessWidget {
|
|||
onLongPressUp: () => dashboardViewModel.balanceViewModel.isReversing =
|
||||
!dashboardViewModel.balanceViewModel.isReversing,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 56),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 24, bottom: 16),
|
||||
child: Observer(builder: (_) {
|
||||
return Text(dashboardViewModel.balanceViewModel.asset,
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
dashboardViewModel.balanceViewModel.asset,
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontFamily: 'Lato',
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.displayMedium!
|
||||
.backgroundColor!,
|
||||
height: 1),
|
||||
color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!,
|
||||
height: 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center);
|
||||
})),
|
||||
Observer(builder: (_) {
|
||||
if (dashboardViewModel.balanceViewModel.isShowCard && FeatureFlag.isCakePayEnabled) {
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled)
|
||||
InkWell(
|
||||
onTap: () => Navigator.pushNamed(context, Routes.homeSettings,
|
||||
arguments: dashboardViewModel.balanceViewModel),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Image.asset(
|
||||
'assets/images/home_screen_settings_icon.png',
|
||||
color:
|
||||
Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) {
|
||||
if (dashboardViewModel.balanceViewModel.isShowCard &&
|
||||
FeatureFlag.isCakePayEnabled) {
|
||||
return IntroducingCard(
|
||||
title: S.of(context).introducing_cake_pay,
|
||||
subTitle: S.of(context).cake_pay_learn_more,
|
||||
|
@ -54,8 +78,10 @@ class BalancePage extends StatelessWidget {
|
|||
closeCard: dashboardViewModel.balanceViewModel.disableIntroCakePayCard);
|
||||
}
|
||||
return Container();
|
||||
}),
|
||||
Observer(builder: (_) {
|
||||
},
|
||||
),
|
||||
Observer(
|
||||
builder: (_) {
|
||||
return ListView.separated(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
|
@ -64,7 +90,8 @@ class BalancePage extends StatelessWidget {
|
|||
itemBuilder: (__, index) {
|
||||
final balance =
|
||||
dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index);
|
||||
return buildBalanceRow(context,
|
||||
return buildBalanceRow(
|
||||
context,
|
||||
availableBalanceLabel:
|
||||
'${dashboardViewModel.balanceViewModel.availableBalanceLabel}',
|
||||
availableBalance: balance.availableBalance,
|
||||
|
@ -75,14 +102,23 @@ class BalancePage extends StatelessWidget {
|
|||
additionalFiatBalance: balance.fiatAdditionalBalance,
|
||||
frozenBalance: balance.frozenBalance,
|
||||
frozenFiatBalance: balance.fiatFrozenBalance,
|
||||
currency: balance.formattedAssetTitle);
|
||||
});
|
||||
})
|
||||
])));
|
||||
currency: balance.formattedAssetTitle,
|
||||
hasAdditionalBalance:
|
||||
dashboardViewModel.balanceViewModel.hasAdditionalBalance,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildBalanceRow(BuildContext context,
|
||||
{required String availableBalanceLabel,
|
||||
Widget buildBalanceRow(
|
||||
BuildContext context, {
|
||||
required String availableBalanceLabel,
|
||||
required String availableBalance,
|
||||
required String availableFiatBalance,
|
||||
required String additionalBalanceLabel,
|
||||
|
@ -90,7 +126,9 @@ class BalancePage extends StatelessWidget {
|
|||
required String additionalFiatBalance,
|
||||
required String frozenBalance,
|
||||
required String frozenFiatBalance,
|
||||
required String currency}) {
|
||||
required String currency,
|
||||
required bool hasAdditionalBalance,
|
||||
}) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 16, right: 16),
|
||||
decoration: BoxDecoration(
|
||||
|
@ -101,7 +139,8 @@ class BalancePage extends StatelessWidget {
|
|||
: Colors.transparent,
|
||||
width: 1,
|
||||
),
|
||||
color: Theme.of(context).textTheme!.titleLarge!.backgroundColor!),
|
||||
color: Theme.of(context).textTheme.titleLarge!.backgroundColor!,
|
||||
),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 16, left: 24, right: 24, bottom: 24),
|
||||
child: Column(
|
||||
|
@ -113,7 +152,7 @@ class BalancePage extends StatelessWidget {
|
|||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => _showBalanceDescription(context),
|
||||
onTap: hasAdditionalBalance ? () => _showBalanceDescription(context) : null,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -129,6 +168,7 @@ class BalancePage extends StatelessWidget {
|
|||
.displaySmall!
|
||||
.backgroundColor!,
|
||||
height: 1)),
|
||||
if (hasAdditionalBalance)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Icon(Icons.help_outline,
|
||||
|
@ -137,11 +177,10 @@ class BalancePage extends StatelessWidget {
|
|||
.accentTextTheme!
|
||||
.displaySmall!
|
||||
.backgroundColor!),
|
||||
)
|
||||
],
|
||||
),SizedBox(
|
||||
height: 6,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
AutoSizeText(availableBalance,
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
|
@ -154,9 +193,7 @@ class BalancePage extends StatelessWidget {
|
|||
height: 1),
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.start),
|
||||
SizedBox(
|
||||
height: 6,
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
Text('${availableFiatBalance}',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
|
@ -168,7 +205,6 @@ class BalancePage extends StatelessWidget {
|
|||
.displayMedium!
|
||||
.backgroundColor!,
|
||||
height: 1)),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -177,40 +213,39 @@ class BalancePage extends StatelessWidget {
|
|||
fontSize: 28,
|
||||
fontFamily: 'Lato',
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.displayMedium!
|
||||
.backgroundColor!,
|
||||
color: Theme.of(context).accentTextTheme!.displayMedium!.backgroundColor!,
|
||||
height: 1)),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 26),
|
||||
if (frozenBalance.isNotEmpty)
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(S.current.frozen_balance,
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 26),
|
||||
Text(
|
||||
S.current.frozen_balance,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: 'Lato',
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.displaySmall!
|
||||
.backgroundColor!,
|
||||
height: 1)),
|
||||
color: Theme.of(context).accentTextTheme.displaySmall!.backgroundColor!,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
AutoSizeText(frozenBalance,
|
||||
AutoSizeText(
|
||||
frozenBalance,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontFamily: 'Lato',
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.displayMedium!
|
||||
.backgroundColor!,
|
||||
height: 1),
|
||||
color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!,
|
||||
height: 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
frozenFiatBalance,
|
||||
|
@ -219,41 +254,42 @@ class BalancePage extends StatelessWidget {
|
|||
fontSize: 12,
|
||||
fontFamily: 'Lato',
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.displayMedium!
|
||||
.backgroundColor!,
|
||||
height: 1),
|
||||
color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!,
|
||||
height: 1,
|
||||
),
|
||||
SizedBox(height: 24)
|
||||
]),
|
||||
Text('${additionalBalanceLabel}',
|
||||
),
|
||||
],
|
||||
),
|
||||
if (hasAdditionalBalance)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 24),
|
||||
Text(
|
||||
'${additionalBalanceLabel}',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: 'Lato',
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.displaySmall!
|
||||
.backgroundColor!,
|
||||
height: 1)),
|
||||
color: Theme.of(context).accentTextTheme.displaySmall!.backgroundColor!,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
AutoSizeText(additionalBalance,
|
||||
AutoSizeText(
|
||||
additionalBalance,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontFamily: 'Lato',
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.displayMedium!
|
||||
.backgroundColor!,
|
||||
height: 1),
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center),
|
||||
SizedBox(
|
||||
height: 4,
|
||||
color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!,
|
||||
height: 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'${additionalFiatBalance}',
|
||||
textAlign: TextAlign.center,
|
||||
|
@ -261,13 +297,15 @@ class BalancePage extends StatelessWidget {
|
|||
fontSize: 12,
|
||||
fontFamily: 'Lato',
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.displayMedium!
|
||||
.backgroundColor!,
|
||||
height: 1),
|
||||
)
|
||||
])),
|
||||
color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,8 @@ class MenuWidgetState extends State<MenuWidget> {
|
|||
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.havenIcon = Image.asset('assets/images/haven_menu.png'),
|
||||
this.ethereumIcon = Image.asset('assets/images/eth_icon.png');
|
||||
|
||||
final largeScreen = 731;
|
||||
|
||||
|
@ -46,6 +47,7 @@ class MenuWidgetState extends State<MenuWidget> {
|
|||
Image bitcoinIcon;
|
||||
Image litecoinIcon;
|
||||
Image havenIcon;
|
||||
Image ethereumIcon;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -85,16 +87,14 @@ class MenuWidgetState extends State<MenuWidget> {
|
|||
|
||||
moneroIcon = Image.asset('assets/images/monero_menu.png',
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.accentTextTheme
|
||||
.labelSmall!
|
||||
.decorationColor!);
|
||||
bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png',
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.accentTextTheme
|
||||
.labelSmall!
|
||||
.decorationColor!);
|
||||
litecoinIcon = Image.asset('assets/images/litecoin_menu.png');
|
||||
havenIcon = Image.asset('assets/images/haven_menu.png');
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
|
@ -215,6 +215,8 @@ class MenuWidgetState extends State<MenuWidget> {
|
|||
return litecoinIcon;
|
||||
case WalletType.haven:
|
||||
return havenIcon;
|
||||
case WalletType.ethereum:
|
||||
return ethereumIcon;
|
||||
default:
|
||||
throw Exception('No icon for ${type.toString()}');
|
||||
}
|
||||
|
|
|
@ -511,17 +511,16 @@ class ExchangeCardState extends State<ExchangeCard> {
|
|||
|
||||
void _presentPicker(BuildContext context) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (_) => CurrencyPicker(
|
||||
selectedAtIndex: widget.currencies.indexOf(_selectedCurrency),
|
||||
items: widget.currencies,
|
||||
hintText: S.of(context).search_currency,
|
||||
isMoneroWallet: _isMoneroWallet,
|
||||
isConvertFrom: widget.hasRefundAddress,
|
||||
onItemSelected: (Currency item) =>
|
||||
widget.onCurrencySelected != null
|
||||
? widget.onCurrencySelected(item as CryptoCurrency)
|
||||
: null),
|
||||
context: context);
|
||||
onItemSelected: (Currency item) => widget.onCurrencySelected(item as CryptoCurrency),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAmountPopup(BuildContext context, PaymentRequest paymentRequest) {
|
||||
|
|
|
@ -11,14 +11,17 @@ import 'package:cw_core/wallet_type.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class NewWalletTypePage extends BasePage {
|
||||
NewWalletTypePage({required this.onTypeSelected});
|
||||
NewWalletTypePage({required this.onTypeSelected, required this.isCreate});
|
||||
|
||||
final void Function(BuildContext, WalletType) onTypeSelected;
|
||||
final bool isCreate;
|
||||
|
||||
final walletTypeImage = Image.asset('assets/images/wallet_type.png');
|
||||
final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png');
|
||||
|
||||
@override
|
||||
String get title => S.current.wallet_list_restore_wallet;
|
||||
String get title =>
|
||||
isCreate ? S.current.wallet_list_create_new_wallet : S.current.wallet_list_restore_wallet;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) => WalletTypeForm(
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:cake_wallet/src/screens/restore/widgets/restore_button.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
|
||||
class RestoreWalletOptionsPage extends BasePage {
|
||||
RestoreWalletOptionsPage(
|
||||
{required this.type,
|
||||
required this.onRestoreFromSeed,
|
||||
required this.onRestoreFromKeys});
|
||||
|
||||
final WalletType type;
|
||||
final Function(BuildContext context) onRestoreFromSeed;
|
||||
final Function(BuildContext context) onRestoreFromKeys;
|
||||
|
||||
@override
|
||||
String get title => S.current.restore_restore_wallet;
|
||||
|
||||
final imageSeed = Image.asset('assets/images/restore_seed.png');
|
||||
final imageKeys = Image.asset('assets/images/restore_keys.png');
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
padding: EdgeInsets.all(24),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
RestoreButton(
|
||||
onPressed: () => onRestoreFromSeed(context),
|
||||
image: imageSeed,
|
||||
title: S.of(context).restore_title_from_seed,
|
||||
description: _fromSeedDescription(context)),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
child: RestoreButton(
|
||||
onPressed: () => onRestoreFromKeys(context),
|
||||
image: imageKeys,
|
||||
title: _fromKeyTitle(context),
|
||||
description: _fromKeyDescription(context)),
|
||||
)
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
String _fromSeedDescription(BuildContext context) {
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
return S.of(context).restore_description_from_seed;
|
||||
case WalletType.bitcoin:
|
||||
// TODO: Add transaction for bitcoin description.
|
||||
return S.of(context).restore_bitcoin_description_from_seed;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
String _fromKeyDescription(BuildContext context) {
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
return S.of(context).restore_description_from_keys;
|
||||
case WalletType.bitcoin:
|
||||
// TODO: Add transaction for bitcoin description.
|
||||
return S.of(context).restore_bitcoin_description_from_keys;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
String _fromKeyTitle(BuildContext context) {
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
return S.of(context).restore_title_from_keys;
|
||||
case WalletType.bitcoin:
|
||||
// TODO: Add transaction for bitcoin description.
|
||||
return S.of(context).restore_bitcoin_title_from_keys;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,9 +11,7 @@ class PreSeedPage extends BasePage {
|
|||
PreSeedPage(this.type)
|
||||
: imageLight = Image.asset('assets/images/pre_seed_light.png'),
|
||||
imageDark = Image.asset('assets/images/pre_seed_dark.png'),
|
||||
wordsCount = type == WalletType.monero
|
||||
? 25
|
||||
: 24; // FIXME: Stupid fast implementation
|
||||
wordsCount = _wordsCount(type);
|
||||
|
||||
final Image imageDark;
|
||||
final Image imageLight;
|
||||
|
@ -68,4 +66,15 @@ class PreSeedPage extends BasePage {
|
|||
),
|
||||
));
|
||||
}
|
||||
|
||||
static int _wordsCount(WalletType type) {
|
||||
switch (type) {
|
||||
case WalletType.monero:
|
||||
return 25;
|
||||
case WalletType.ethereum:
|
||||
return 12;
|
||||
default:
|
||||
return 24;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -220,7 +220,6 @@ class SendPage extends BasePage {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (sendViewModel.hasMultiRecipient)
|
||||
Container(
|
||||
height: 40,
|
||||
width: double.infinity,
|
||||
|
@ -250,10 +249,8 @@ class SendPage extends BasePage {
|
|||
key: UniqueKey(),
|
||||
to: template.name,
|
||||
hasMultipleRecipients:
|
||||
template.additionalRecipients !=
|
||||
null &&
|
||||
template.additionalRecipients!
|
||||
.length > 1,
|
||||
template.additionalRecipients != null &&
|
||||
template.additionalRecipients!.length > 1,
|
||||
amount: template.isCurrencySelected
|
||||
? template.amount
|
||||
: template.amountFiat,
|
||||
|
@ -261,16 +258,10 @@ class SendPage extends BasePage {
|
|||
? template.cryptoCurrency
|
||||
: template.fiatCurrency,
|
||||
onTap: () async {
|
||||
if (template.additionalRecipients !=
|
||||
null) {
|
||||
if (template.additionalRecipients?.isNotEmpty ?? false) {
|
||||
sendViewModel.clearOutputs();
|
||||
|
||||
template.additionalRecipients!
|
||||
.forEach((currentElement) async {
|
||||
int i = template
|
||||
.additionalRecipients!
|
||||
.indexOf(currentElement);
|
||||
|
||||
for (int i = 0;i < template.additionalRecipients!.length;i++) {
|
||||
Output output;
|
||||
try {
|
||||
output = sendViewModel.outputs[i];
|
||||
|
@ -282,14 +273,16 @@ class SendPage extends BasePage {
|
|||
await _setInputsFromTemplate(
|
||||
context,
|
||||
output: output,
|
||||
template: currentElement);
|
||||
});
|
||||
template: template.additionalRecipients![i],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
final output = _defineCurrentOutput();
|
||||
await _setInputsFromTemplate(
|
||||
context,
|
||||
output: output,
|
||||
template: template);
|
||||
template: template,
|
||||
);
|
||||
}
|
||||
},
|
||||
onRemove: () {
|
||||
|
@ -328,7 +321,7 @@ class SendPage extends BasePage {
|
|||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -350,7 +343,7 @@ class SendPage extends BasePage {
|
|||
.displaySmall!
|
||||
.decorationColor!,
|
||||
))),
|
||||
if (sendViewModel.hasMultiRecipient)
|
||||
if (sendViewModel.sendTemplateViewModel.hasMultiRecipient)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 12),
|
||||
child: PrimaryButton(
|
||||
|
@ -518,6 +511,7 @@ class SendPage extends BasePage {
|
|||
output.address = template.address;
|
||||
|
||||
if (template.isCurrencySelected) {
|
||||
sendViewModel.setSelectedCryptoCurrency(template.cryptoCurrency);
|
||||
output.setCryptoAmount(template.amount);
|
||||
} else {
|
||||
sendViewModel.setFiatCurrency(fiatFromTemplate);
|
||||
|
|
|
@ -67,8 +67,7 @@ class SendTemplatePage extends BasePage {
|
|||
controller: controller,
|
||||
itemCount: sendTemplateViewModel.recipients.length,
|
||||
itemBuilder: (_, index) {
|
||||
final template =
|
||||
sendTemplateViewModel.recipients[index];
|
||||
final template = sendTemplateViewModel.recipients[index];
|
||||
return SendTemplateCard(
|
||||
template: template,
|
||||
index: index,
|
||||
|
@ -76,8 +75,7 @@ class SendTemplatePage extends BasePage {
|
|||
});
|
||||
})),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 10, left: 24, right: 24, bottom: 10),
|
||||
padding: EdgeInsets.only(top: 10, left: 24, right: 24, bottom: 10),
|
||||
child: Container(
|
||||
height: 10,
|
||||
child: Observer(
|
||||
|
@ -107,55 +105,42 @@ class SendTemplatePage extends BasePage {
|
|||
),
|
||||
),
|
||||
])),
|
||||
bottomSectionPadding:
|
||||
EdgeInsets.only(left: 24, right: 24, bottom: 24),
|
||||
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
|
||||
bottomSection: Column(children: [
|
||||
// if (sendViewModel.hasMultiRecipient)
|
||||
if (sendTemplateViewModel.hasMultiRecipient)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 12),
|
||||
child: PrimaryButton(
|
||||
onPressed: () {
|
||||
sendTemplateViewModel.addRecipient();
|
||||
Future.delayed(const Duration(milliseconds: 250), () {
|
||||
controller.jumpToPage(
|
||||
sendTemplateViewModel.recipients.length - 1);
|
||||
controller.jumpToPage(sendTemplateViewModel.recipients.length - 1);
|
||||
});
|
||||
},
|
||||
text: S.of(context).add_receiver,
|
||||
color: Colors.transparent,
|
||||
textColor: Theme.of(context)
|
||||
.accentTextTheme
|
||||
.displaySmall!
|
||||
.decorationColor!,
|
||||
textColor: Theme.of(context).accentTextTheme.displaySmall!.decorationColor!,
|
||||
isDottedBorder: true,
|
||||
borderColor: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.displaySmall!
|
||||
.decorationColor!)),
|
||||
borderColor:
|
||||
Theme.of(context).primaryTextTheme.displaySmall!.decorationColor!)),
|
||||
PrimaryButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState != null &&
|
||||
_formKey.currentState!.validate()) {
|
||||
if (_formKey.currentState != null && _formKey.currentState!.validate()) {
|
||||
final mainTemplate = sendTemplateViewModel.recipients[0];
|
||||
print(sendTemplateViewModel.recipients.map((element) =>
|
||||
element.toTemplate(
|
||||
cryptoCurrency:
|
||||
sendTemplateViewModel.cryptoCurrency.title,
|
||||
fiatCurrency:
|
||||
sendTemplateViewModel.fiatCurrency)));
|
||||
final additionalRecipients = sendTemplateViewModel.recipients
|
||||
.map((element) => element.toTemplate(
|
||||
cryptoCurrency: element.selectedCurrency.title,
|
||||
fiatCurrency: sendTemplateViewModel.fiatCurrency))
|
||||
.toList();
|
||||
|
||||
sendTemplateViewModel.addTemplate(
|
||||
isCurrencySelected: mainTemplate.isCurrencySelected,
|
||||
name: mainTemplate.name,
|
||||
address: mainTemplate.address,
|
||||
cryptoCurrency: mainTemplate.selectedCurrency.title,
|
||||
amount: mainTemplate.output.cryptoAmount,
|
||||
amountFiat: mainTemplate.output.fiatAmount,
|
||||
additionalRecipients: sendTemplateViewModel.recipients
|
||||
.map((element) => element.toTemplate(
|
||||
cryptoCurrency: sendTemplateViewModel
|
||||
.cryptoCurrency.title,
|
||||
fiatCurrency:
|
||||
sendTemplateViewModel.fiatCurrency))
|
||||
.toList());
|
||||
additionalRecipients: additionalRecipients);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -4,29 +4,53 @@ class PrefixCurrencyIcon extends StatelessWidget {
|
|||
PrefixCurrencyIcon({
|
||||
required this.isSelected,
|
||||
required this.title,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final bool isSelected;
|
||||
final String title;
|
||||
final Function()? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(0, 6.0, 8.0, 0),
|
||||
child: Column(children: [
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
color: isSelected ? Colors.green : Colors.transparent,
|
||||
),
|
||||
child: Text(title + ':',
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (onTap != null)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 5),
|
||||
child: Image.asset(
|
||||
'assets/images/arrow_bottom_purple_icon.png',
|
||||
color: Colors.white,
|
||||
height: 8,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
title + ':',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
)),
|
||||
)
|
||||
]));
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/utils/payment_request.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
|
||||
|
@ -38,12 +41,8 @@ class SendCard extends StatefulWidget {
|
|||
);
|
||||
}
|
||||
|
||||
class SendCardState extends State<SendCard>
|
||||
with AutomaticKeepAliveClientMixin<SendCard> {
|
||||
SendCardState({
|
||||
required this.output,
|
||||
required this.sendViewModel,
|
||||
this.initialPaymentRequest})
|
||||
class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<SendCard> {
|
||||
SendCardState({required this.output, required this.sendViewModel, this.initialPaymentRequest})
|
||||
: addressController = TextEditingController(),
|
||||
cryptoAmountController = TextEditingController(),
|
||||
fiatAmountController = TextEditingController(),
|
||||
|
@ -102,10 +101,7 @@ class SendCardState extends State<SendCard>
|
|||
KeyboardActions(
|
||||
config: KeyboardActionsConfig(
|
||||
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
|
||||
keyboardBarColor: Theme.of(context)
|
||||
.accentTextTheme
|
||||
.bodyLarge!
|
||||
.backgroundColor!,
|
||||
keyboardBarColor: Theme.of(context).accentTextTheme.bodyLarge!.backgroundColor!,
|
||||
nextFocus: false,
|
||||
actions: [
|
||||
KeyboardActionsItem(
|
||||
|
@ -116,24 +112,28 @@ class SendCardState extends State<SendCard>
|
|||
focusNode: fiatAmountFocus,
|
||||
toolbarButtons: [(_) => KeyboardDoneButton()],
|
||||
)
|
||||
]),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
height: 0,
|
||||
color: Colors.transparent,
|
||||
)),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: ResponsiveLayoutUtil.instance.isMobile ? BoxDecoration(
|
||||
decoration: ResponsiveLayoutUtil.instance.isMobile
|
||||
? BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(24),
|
||||
bottomRight: Radius.circular(24)),
|
||||
gradient: LinearGradient(colors: [
|
||||
bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).primaryTextTheme.titleMedium!.color!,
|
||||
Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.titleMedium!
|
||||
.decorationColor!,
|
||||
], begin: Alignment.topLeft, end: Alignment.bottomRight),
|
||||
) : null,
|
||||
Theme.of(context).primaryTextTheme.titleMedium!.decorationColor!,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
24,
|
||||
|
@ -142,7 +142,8 @@ class SendCardState extends State<SendCard>
|
|||
ResponsiveLayoutUtil.instance.isMobile ? 32 : 0,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Observer(builder: (_) => Column(
|
||||
child: Observer(
|
||||
builder: (_) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Observer(builder: (_) {
|
||||
|
@ -164,25 +165,15 @@ class SendCardState extends State<SendCard>
|
|||
AddressTextFieldOption.qrCode,
|
||||
AddressTextFieldOption.addressBook
|
||||
],
|
||||
buttonColor: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineMedium!
|
||||
.color!,
|
||||
borderColor: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.color!,
|
||||
buttonColor: Theme.of(context).primaryTextTheme.headlineMedium!.color!,
|
||||
borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!,
|
||||
textStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white),
|
||||
fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.decorationColor!),
|
||||
color:
|
||||
Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!),
|
||||
onPushPasteButton: (context) async {
|
||||
output.resetParsedAddress();
|
||||
await output.fetchParsedAddress(context);
|
||||
|
@ -197,22 +188,16 @@ class SendCardState extends State<SendCard>
|
|||
selectedCurrency: sendViewModel.currency,
|
||||
);
|
||||
}),
|
||||
if (output.isParsedAddress) Padding(
|
||||
if (output.isParsedAddress)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: BaseTextFormField(
|
||||
controller: extractedAddressController,
|
||||
readOnly: true,
|
||||
borderColor: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.color!,
|
||||
borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!,
|
||||
textStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white),
|
||||
validator: sendViewModel.addressValidator
|
||||
)
|
||||
),
|
||||
fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
|
||||
validator: sendViewModel.addressValidator)),
|
||||
Observer(
|
||||
builder: (_) => Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
|
@ -222,14 +207,44 @@ class SendCardState extends State<SendCard>
|
|||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
sendViewModel.hasMultipleTokens
|
||||
? Container(
|
||||
padding: EdgeInsets.only(right: 8),
|
||||
height: 32,
|
||||
child: InkWell(
|
||||
onTap: () => _presentPicker(context),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 5),
|
||||
child: Image.asset(
|
||||
'assets/images/arrow_bottom_purple_icon.png',
|
||||
color: Colors.white,
|
||||
height: 8,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
sendViewModel.selectedCryptoCurrency.title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
)),
|
||||
sendViewModel.selectedCryptoCurrency.tag != null ? Padding(
|
||||
fontSize: 16,
|
||||
color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
sendViewModel.selectedCryptoCurrency.title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
color: Colors.white),
|
||||
),
|
||||
sendViewModel.selectedCryptoCurrency.tag != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.fromLTRB(3.0, 0, 3.0, 0),
|
||||
child: Container(
|
||||
height: 32,
|
||||
|
@ -238,30 +253,36 @@ class SendCardState extends State<SendCard>
|
|||
.primaryTextTheme
|
||||
.headlineMedium!
|
||||
.color!,
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(6))),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(6),
|
||||
)),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Text( sendViewModel.selectedCryptoCurrency.tag!,
|
||||
child: Text(
|
||||
sendViewModel.selectedCryptoCurrency.tag!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineMedium!
|
||||
.decorationColor!)),
|
||||
.decorationColor!),
|
||||
),
|
||||
),
|
||||
),
|
||||
) : Container(),
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: Text(':',
|
||||
child: Text(
|
||||
':',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
color: Colors.white)),
|
||||
color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -272,8 +293,7 @@ class SendCardState extends State<SendCard>
|
|||
BaseTextFormField(
|
||||
focusNode: cryptoAmountFocus,
|
||||
controller: cryptoAmountController,
|
||||
keyboardType:
|
||||
TextInputType.numberWithOptions(
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
signed: false, decimal: true),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))
|
||||
|
@ -296,61 +316,64 @@ class SendCardState extends State<SendCard>
|
|||
fontSize: 14),
|
||||
validator: output.sendAll
|
||||
? sendViewModel.allAmountValidator
|
||||
: sendViewModel
|
||||
.amountValidator),
|
||||
if (!sendViewModel.isBatchSending) Positioned(
|
||||
: sendViewModel.amountValidator,
|
||||
),
|
||||
if (!sendViewModel.isBatchSending)
|
||||
Positioned(
|
||||
top: 2,
|
||||
right: 0,
|
||||
child: Container(
|
||||
width: prefixIconWidth,
|
||||
height: prefixIconHeight,
|
||||
child: InkWell(
|
||||
onTap: () async =>
|
||||
output.setSendAll(),
|
||||
onTap: () async => output.setSendAll(),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineMedium!
|
||||
.color!,
|
||||
borderRadius:
|
||||
BorderRadius.all(
|
||||
Radius.circular(6))),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(6),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
S.of(context).all,
|
||||
textAlign:
|
||||
TextAlign.center,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
color:
|
||||
Theme.of(context)
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineMedium!
|
||||
.decorationColor!))),
|
||||
))))]),
|
||||
.decorationColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
)),
|
||||
Divider(height: 1,color: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.decorationColor!),
|
||||
),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!),
|
||||
Observer(
|
||||
builder: (_) => Padding(
|
||||
padding: EdgeInsets.only(top: 10),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
S.of(context).available_balance +
|
||||
':',
|
||||
S.of(context).available_balance + ':',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
@ -358,7 +381,8 @@ class SendCardState extends State<SendCard>
|
|||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.decorationColor!),
|
||||
)),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
sendViewModel.balance,
|
||||
style: TextStyle(
|
||||
|
@ -371,7 +395,8 @@ class SendCardState extends State<SendCard>
|
|||
)
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
if (!sendViewModel.isFiatDisabled)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
|
@ -379,81 +404,70 @@ class SendCardState extends State<SendCard>
|
|||
focusNode: fiatAmountFocus,
|
||||
controller: fiatAmountController,
|
||||
keyboardType:
|
||||
TextInputType.numberWithOptions(
|
||||
signed: false, decimal: true),
|
||||
TextInputType.numberWithOptions(signed: false, decimal: true),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))
|
||||
FilteringTextInputFormatter.deny(
|
||||
RegExp('[\\-|\\ ]'),
|
||||
)
|
||||
],
|
||||
prefixIcon: Padding(
|
||||
padding: EdgeInsets.only(top: 9),
|
||||
child:
|
||||
Text(sendViewModel.fiat.title + ':',
|
||||
child: Text(
|
||||
sendViewModel.fiat.title + ':',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
hintText: '0.00',
|
||||
borderColor: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.color!,
|
||||
borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!,
|
||||
textStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white),
|
||||
fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
|
||||
placeholderTextStyle: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme.headlineSmall!.decorationColor!,
|
||||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.decorationColor!,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14),
|
||||
)),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
child: BaseTextFormField(
|
||||
controller: noteController,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
borderColor: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.color!,
|
||||
borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!,
|
||||
textStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white),
|
||||
fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
|
||||
hintText: S.of(context).note_optional,
|
||||
placeholderTextStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.decorationColor!),
|
||||
color:
|
||||
Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!),
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => GestureDetector(
|
||||
onTap: () =>
|
||||
_setTransactionPriority(context),
|
||||
onTap: () => _setTransactionPriority(context),
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
S
|
||||
.of(context)
|
||||
.send_estimated_fee,
|
||||
S.of(context).send_estimated_fee,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight:
|
||||
FontWeight.w500,
|
||||
fontWeight: FontWeight.w500,
|
||||
//color: Theme.of(context).primaryTextTheme!.displaySmall!.color!,
|
||||
color: Colors.white)),
|
||||
color: Colors.white),
|
||||
),
|
||||
Container(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
@ -463,45 +477,38 @@ class SendCardState extends State<SendCard>
|
|||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
output
|
||||
.estimatedFee
|
||||
.toString() +
|
||||
output.estimatedFee.toString() +
|
||||
' ' +
|
||||
sendViewModel
|
||||
.selectedCryptoCurrency.toString(),
|
||||
sendViewModel.selectedCryptoCurrency.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight:
|
||||
FontWeight.w600,
|
||||
fontWeight: FontWeight.w600,
|
||||
//color: Theme.of(context).primaryTextTheme!.displaySmall!.color!,
|
||||
color:
|
||||
Colors.white)),
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
EdgeInsets.only(top: 5),
|
||||
padding: EdgeInsets.only(top: 5),
|
||||
child: sendViewModel.isFiatDisabled
|
||||
? const SizedBox(height: 14)
|
||||
: Text(output
|
||||
.estimatedFeeFiatAmount
|
||||
+ ' ' +
|
||||
sendViewModel
|
||||
.fiat.title,
|
||||
: Text(
|
||||
output.estimatedFeeFiatAmount +
|
||||
' ' +
|
||||
sendViewModel.fiat.title,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight:
|
||||
FontWeight.w600,
|
||||
color: Theme
|
||||
.of(context)
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.decorationColor!))
|
||||
.decorationColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 2,
|
||||
left: 5),
|
||||
padding: EdgeInsets.only(top: 2, left: 5),
|
||||
child: Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
|
@ -514,36 +521,38 @@ class SendCardState extends State<SendCard>
|
|||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
if (sendViewModel.isElectrumWallet) Padding(
|
||||
),
|
||||
),
|
||||
if (sendViewModel.isElectrumWallet)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 6),
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushNamed(Routes.unspentCoinsList),
|
||||
onTap: () => Navigator.of(context).pushNamed(Routes.unspentCoinsList),
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).coin_control,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white)),
|
||||
color: Colors.white),
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Colors.white,
|
||||
)
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
))
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -664,6 +673,7 @@ class SendCardState extends State<SendCard>
|
|||
final selectedItem = items.indexOf(sendViewModel.transactionPriority);
|
||||
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (_) => Picker(
|
||||
items: items,
|
||||
displayItem: sendViewModel.displayFeeRate,
|
||||
|
@ -673,7 +683,20 @@ class SendCardState extends State<SendCard>
|
|||
onItemSelected: (TransactionPriority priority) =>
|
||||
sendViewModel.setTransactionPriority(priority),
|
||||
),
|
||||
context: context);
|
||||
);
|
||||
}
|
||||
|
||||
void _presentPicker(BuildContext context) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (_) => CurrencyPicker(
|
||||
selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency),
|
||||
items: sendViewModel.currencies,
|
||||
hintText: S.of(context).search_currency,
|
||||
onItemSelected: (Currency cur) =>
|
||||
sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
||||
import 'package:cake_wallet/src/screens/send/widgets/prefix_currency_icon_widget.dart';
|
||||
import 'package:cake_wallet/utils/payment_request.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/view_model/send/template_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
@ -36,36 +40,29 @@ class SendTemplateCard extends StatelessWidget {
|
|||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(24),
|
||||
bottomRight: Radius.circular(24)),
|
||||
borderRadius:
|
||||
BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
|
||||
gradient: LinearGradient(colors: [
|
||||
Theme.of(context).primaryTextTheme.titleMedium!.color!,
|
||||
Theme.of(context).primaryTextTheme.titleMedium!.decorationColor!
|
||||
], begin: Alignment.topLeft, end: Alignment.bottomRight)),
|
||||
child: Column(children: <Widget>[
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(24, 90, 24, 32),
|
||||
child: Column(children: <Widget>[
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
if (index == 0)
|
||||
BaseTextFormField(
|
||||
controller: _nameController,
|
||||
hintText: sendTemplateViewModel.recipients.length > 1
|
||||
? S.of(context).template_name
|
||||
: S.of(context).send_name,
|
||||
borderColor: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.color!,
|
||||
textStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white),
|
||||
borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!,
|
||||
textStyle:
|
||||
TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
|
||||
placeholderTextStyle: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.decorationColor!,
|
||||
color: Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14),
|
||||
validator: sendTemplateViewModel.templateValidator),
|
||||
|
@ -92,26 +89,21 @@ class SendTemplateCard extends StatelessWidget {
|
|||
template.output.resetParsedAddress();
|
||||
await template.output.fetchParsedAddress(context);
|
||||
},
|
||||
buttonColor: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineMedium!
|
||||
.color!,
|
||||
borderColor: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.color!,
|
||||
buttonColor: Theme.of(context).primaryTextTheme.headlineMedium!.color!,
|
||||
borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!,
|
||||
textStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white),
|
||||
color: Colors.white,
|
||||
),
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.decorationColor!),
|
||||
validator: sendTemplateViewModel.addressValidator)),
|
||||
color: Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!,
|
||||
),
|
||||
validator: sendTemplateViewModel.addressValidator,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: Focus(
|
||||
|
@ -123,34 +115,29 @@ class SendTemplateCard extends StatelessWidget {
|
|||
child: BaseTextFormField(
|
||||
focusNode: _cryptoAmountFocus,
|
||||
controller: _cryptoAmountController,
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
signed: false, decimal: true),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.deny(
|
||||
RegExp('[\\-|\\ ]'))
|
||||
],
|
||||
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
|
||||
inputFormatters: [FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))],
|
||||
prefixIcon: Observer(
|
||||
builder: (_) => PrefixCurrencyIcon(
|
||||
title: sendTemplateViewModel
|
||||
.cryptoCurrency.title,
|
||||
isSelected: template.isCurrencySelected)),
|
||||
title: template.selectedCurrency.title,
|
||||
isSelected: template.isCurrencySelected,
|
||||
onTap: sendTemplateViewModel.walletCurrencies.length > 1
|
||||
? () => _presentPicker(context)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
hintText: '0.0000',
|
||||
borderColor: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.color!,
|
||||
textStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white),
|
||||
borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!,
|
||||
textStyle:
|
||||
TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
|
||||
placeholderTextStyle: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.decorationColor!,
|
||||
color: Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14),
|
||||
validator: sendTemplateViewModel.amountValidator))),
|
||||
validator: sendTemplateViewModel.amountValidator,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: Focus(
|
||||
|
@ -162,34 +149,30 @@ class SendTemplateCard extends StatelessWidget {
|
|||
child: BaseTextFormField(
|
||||
focusNode: _fiatAmountFocus,
|
||||
controller: _fiatAmountController,
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
signed: false, decimal: true),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.deny(
|
||||
RegExp('[\\-|\\ ]'))
|
||||
],
|
||||
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
|
||||
inputFormatters: [FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))],
|
||||
prefixIcon: Observer(
|
||||
builder: (_) => PrefixCurrencyIcon(
|
||||
title: sendTemplateViewModel.fiatCurrency,
|
||||
isSelected: template.isFiatSelected)),
|
||||
hintText: '0.00',
|
||||
borderColor: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.color!,
|
||||
textStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white),
|
||||
borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!,
|
||||
textStyle:
|
||||
TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
|
||||
placeholderTextStyle: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.decorationColor!,
|
||||
color: Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14))))
|
||||
]))
|
||||
]));
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _setEffects(BuildContext context) {
|
||||
|
@ -264,4 +247,16 @@ class SendTemplateCard extends StatelessWidget {
|
|||
|
||||
_effectsInstalled = true;
|
||||
}
|
||||
|
||||
void _presentPicker(BuildContext context) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (_) => CurrencyPicker(
|
||||
selectedAtIndex: sendTemplateViewModel.walletCurrencies.indexOf(template.selectedCurrency),
|
||||
items: sendTemplateViewModel.walletCurrencies,
|
||||
hintText: S.of(context).search_currency,
|
||||
onItemSelected: (Currency cur) => template.changeSelectedCurrency(cur as CryptoCurrency),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,8 @@ class PrivacyPage extends BasePage {
|
|||
title: S.current.exchange,
|
||||
items: ExchangeApiMode.all,
|
||||
selectedItem: _privacySettingsViewModel.exchangeStatus,
|
||||
onItemSelected: (ExchangeApiMode mode) => _privacySettingsViewModel.setExchangeApiMode(mode),
|
||||
onItemSelected: (ExchangeApiMode mode) =>
|
||||
_privacySettingsViewModel.setExchangeApiMode(mode),
|
||||
),
|
||||
),
|
||||
SettingsSwitcherCell(
|
||||
|
@ -68,6 +69,13 @@ class PrivacyPage extends BasePage {
|
|||
onValueChange: (BuildContext _, bool value) {
|
||||
_privacySettingsViewModel.setDisableSell(value);
|
||||
}),
|
||||
if (_privacySettingsViewModel.canUseEtherscan)
|
||||
SettingsSwitcherCell(
|
||||
title: S.current.etherscan_history,
|
||||
value: _privacySettingsViewModel.useEtherscan,
|
||||
onValueChange: (BuildContext _, bool value) {
|
||||
_privacySettingsViewModel.setUseEtherscan(value);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}),
|
||||
|
|
|
@ -3,14 +3,23 @@ import 'package:cake_wallet/src/widgets/standard_list.dart';
|
|||
import 'package:cake_wallet/src/widgets/standard_switch.dart';
|
||||
|
||||
class SettingsSwitcherCell extends StandardListRow {
|
||||
SettingsSwitcherCell(
|
||||
{required String title, required this.value, this.onValueChange})
|
||||
: super(title: title, isSelected: false);
|
||||
SettingsSwitcherCell({
|
||||
required String title,
|
||||
required this.value,
|
||||
this.onValueChange,
|
||||
Decoration? decoration,
|
||||
this.leading,
|
||||
void Function(BuildContext context)? onTap,
|
||||
}) : super(title: title, isSelected: false, decoration: decoration, onTap: onTap);
|
||||
|
||||
final bool value;
|
||||
final void Function(BuildContext context, bool value)? onValueChange;
|
||||
final Widget? leading;
|
||||
|
||||
@override
|
||||
Widget buildTrailing(BuildContext context) => StandardSwitch(
|
||||
value: value, onTaped: () => onValueChange?.call(context, !value));
|
||||
Widget buildTrailing(BuildContext context) =>
|
||||
StandardSwitch(value: value, onTaped: () => onValueChange?.call(context, !value));
|
||||
|
||||
@override
|
||||
Widget? buildLeading(BuildContext context) => leading;
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ class WalletListBodyState extends State<WalletListBody> {
|
|||
final litecoinIcon = Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24);
|
||||
final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24);
|
||||
final havenIcon = Image.asset('assets/images/haven_logo.png', height: 24, width: 24);
|
||||
final ethereumIcon = Image.asset('assets/images/eth_icon.png', height: 24, width: 24);
|
||||
final scrollController = ScrollController();
|
||||
final double tileHeight = 60;
|
||||
Flushbar<void>? _progressBar;
|
||||
|
@ -230,6 +231,8 @@ class WalletListBodyState extends State<WalletListBody> {
|
|||
return litecoinIcon;
|
||||
case WalletType.haven:
|
||||
return havenIcon;
|
||||
case WalletType.ethereum:
|
||||
return ethereumIcon;
|
||||
default:
|
||||
return nonWalletTypeIcon;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,8 @@
|
|||
import 'dart:ui';
|
||||
import 'package:cake_wallet/palette.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CheckboxWidget extends StatefulWidget {
|
||||
CheckboxWidget({
|
||||
required this.value,
|
||||
required this.caption,
|
||||
required this.onChanged});
|
||||
CheckboxWidget({required this.value, required this.caption, required this.onChanged});
|
||||
|
||||
final bool value;
|
||||
final String caption;
|
||||
|
@ -26,55 +21,45 @@ class CheckboxWidgetState extends State<CheckboxWidget> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
value = !value;
|
||||
onChanged(value);
|
||||
setState(() {});
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
height: 16,
|
||||
width: 16,
|
||||
height: 24.0,
|
||||
width: 24.0,
|
||||
margin: EdgeInsets.only(right: 10.0),
|
||||
decoration: BoxDecoration(
|
||||
color: value
|
||||
? Palette.blueCraiola
|
||||
: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.titleMedium!
|
||||
.decorationColor!,
|
||||
borderRadius: BorderRadius.all(Radius.circular(2)),
|
||||
border: Border.all(
|
||||
color: value
|
||||
? Palette.blueCraiola
|
||||
: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.labelSmall!
|
||||
.color!,
|
||||
width: 1)),
|
||||
color: Theme.of(context).primaryTextTheme.bodySmall!.color!,
|
||||
width: 1.0,
|
||||
),
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8.0),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
),
|
||||
child: value
|
||||
? Center(
|
||||
child: Icon(
|
||||
Icons.done,
|
||||
color: Colors.white,
|
||||
size: 14,
|
||||
),
|
||||
? Icon(
|
||||
Icons.check,
|
||||
color: Colors.blue,
|
||||
size: 20.0,
|
||||
)
|
||||
: Offstage(),
|
||||
: null,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
caption,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryTextTheme!.titleLarge!.color!,
|
||||
fontSize: 18,
|
||||
fontFamily: 'Lato',
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.none
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14.0,
|
||||
color: Theme.of(context).primaryTextTheme.titleLarge!.color!,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// ignore_for_file: deprecated_member_use
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cake_wallet/src/widgets/search_bar_widget.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -145,8 +147,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
|
|||
borderRadius: BorderRadius.all(Radius.circular(30)),
|
||||
child: Container(
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.titleLarge!
|
||||
.accentTextTheme.titleLarge!
|
||||
.color!,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
|
@ -163,8 +164,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
|
|||
),
|
||||
Divider(
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.titleLarge!
|
||||
.accentTextTheme.titleLarge!
|
||||
.backgroundColor!,
|
||||
height: 1,
|
||||
),
|
||||
|
@ -194,8 +194,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
|
|||
fontFamily: 'Lato',
|
||||
decoration: TextDecoration.none,
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme!
|
||||
.titleLarge!
|
||||
.primaryTextTheme.titleLarge!
|
||||
.color!,
|
||||
),
|
||||
),
|
||||
|
@ -217,8 +216,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
|
|||
Widget itemsList() {
|
||||
return Container(
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.titleLarge!
|
||||
.accentTextTheme.titleLarge!
|
||||
.backgroundColor!,
|
||||
child: widget.isGridView
|
||||
? GridView.builder(
|
||||
|
@ -240,8 +238,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
|
|||
separatorBuilder: (context, index) => widget.isSeparated
|
||||
? Divider(
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.titleLarge!
|
||||
.accentTextTheme.titleLarge!
|
||||
.backgroundColor!,
|
||||
height: 1,
|
||||
)
|
||||
|
@ -254,15 +251,9 @@ class _PickerState<Item> extends State<Picker<Item>> {
|
|||
|
||||
Widget buildItem(int index) {
|
||||
final item = filteredItems[index];
|
||||
final tag = item is Currency ? item.tag : null;
|
||||
|
||||
final icon = item is Currency && item.iconPath != null
|
||||
? Image.asset(
|
||||
item.iconPath!,
|
||||
height: 20.0,
|
||||
width: 20.0,
|
||||
)
|
||||
: null;
|
||||
final tag = item is Currency ? item.tag : null;
|
||||
final icon = _getItemIcon(item);
|
||||
|
||||
final image = images.isNotEmpty ? filteredImages[index] : icon;
|
||||
|
||||
|
@ -274,8 +265,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
|
|||
child: Container(
|
||||
height: 55,
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.titleLarge!
|
||||
.accentTextTheme.titleLarge!
|
||||
.color!,
|
||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Row(
|
||||
|
@ -298,8 +288,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
|
|||
fontFamily: 'Lato',
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme!
|
||||
.titleLarge!
|
||||
.primaryTextTheme.titleLarge!
|
||||
.color!,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
|
@ -318,8 +307,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
|
|||
fontSize: 7.0,
|
||||
fontFamily: 'Lato',
|
||||
color: Theme.of(context)
|
||||
.textTheme!
|
||||
.bodyMedium!
|
||||
.textTheme.bodyMedium!
|
||||
.color!),
|
||||
),
|
||||
),
|
||||
|
@ -327,8 +315,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
|
|||
borderRadius: BorderRadius.circular(6.0),
|
||||
//border: Border.all(color: ),
|
||||
color: Theme.of(context)
|
||||
.textTheme!
|
||||
.bodyMedium!
|
||||
.textTheme.bodyMedium!
|
||||
.decorationColor!,
|
||||
),
|
||||
),
|
||||
|
@ -345,15 +332,9 @@ class _PickerState<Item> extends State<Picker<Item>> {
|
|||
|
||||
Widget buildSelectedItem(int index) {
|
||||
final item = items[index];
|
||||
final tag = item is Currency ? item.tag : null;
|
||||
|
||||
final icon = item is Currency && item.iconPath != null
|
||||
? Image.asset(
|
||||
item.iconPath!,
|
||||
height: 20.0,
|
||||
width: 20.0,
|
||||
)
|
||||
: null;
|
||||
final tag = item is Currency ? item.tag : null;
|
||||
final icon = _getItemIcon(item);
|
||||
|
||||
final image = images.isNotEmpty ? images[index] : icon;
|
||||
|
||||
|
@ -364,8 +345,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
|
|||
child: Container(
|
||||
height: 55,
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.titleLarge!
|
||||
.accentTextTheme.titleLarge!
|
||||
.color!,
|
||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Row(
|
||||
|
@ -388,8 +368,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
|
|||
fontFamily: 'Lato',
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme!
|
||||
.titleLarge!
|
||||
.primaryTextTheme.titleLarge!
|
||||
.color!,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
|
@ -408,8 +387,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
|
|||
fontSize: 7.0,
|
||||
fontFamily: 'Lato',
|
||||
color: Theme.of(context)
|
||||
.textTheme!
|
||||
.bodyMedium!
|
||||
.textTheme.bodyMedium!
|
||||
.color!),
|
||||
),
|
||||
),
|
||||
|
@ -417,8 +395,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
|
|||
borderRadius: BorderRadius.circular(6.0),
|
||||
//border: Border.all(color: ),
|
||||
color: Theme.of(context)
|
||||
.textTheme!
|
||||
.bodyMedium!
|
||||
.textTheme.bodyMedium!
|
||||
.decorationColor!,
|
||||
),
|
||||
),
|
||||
|
@ -429,12 +406,40 @@ class _PickerState<Item> extends State<Picker<Item>> {
|
|||
),
|
||||
Icon(Icons.check_circle,
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme!
|
||||
.bodyLarge!
|
||||
.accentTextTheme.bodyLarge!
|
||||
.color!),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _getItemIcon(Item item) {
|
||||
if (item is Currency) {
|
||||
if (item.iconPath != null) {
|
||||
return Image.asset(
|
||||
item.iconPath!,
|
||||
height: 20.0,
|
||||
width: 20.0,
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
height: 20.0,
|
||||
width: 20.0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
item.name.substring(0, min(item.name.length, 2)).toUpperCase(),
|
||||
style: TextStyle(fontSize: 11),
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,11 +5,12 @@ import 'package:flutter/material.dart';
|
|||
|
||||
class StandardListRow extends StatelessWidget {
|
||||
StandardListRow(
|
||||
{required this.title, required this.isSelected, this.onTap});
|
||||
{required this.title, required this.isSelected, this.onTap, this.decoration});
|
||||
|
||||
final String title;
|
||||
final bool isSelected;
|
||||
final void Function(BuildContext context)? onTap;
|
||||
final Decoration? decoration;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -19,9 +20,11 @@ class StandardListRow extends StatelessWidget {
|
|||
return InkWell(
|
||||
onTap: () => onTap?.call(context),
|
||||
child: Container(
|
||||
color: _backgroundColor(context),
|
||||
height: 56,
|
||||
padding: EdgeInsets.only(left: 24, right: 24),
|
||||
decoration: decoration ?? BoxDecoration(
|
||||
color: _backgroundColor(context),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
|
@ -54,7 +57,7 @@ class StandardListRow extends StatelessWidget {
|
|||
|
||||
Color titleColor(BuildContext context) => isSelected
|
||||
? Palette.blueCraiola
|
||||
: Theme.of(context).primaryTextTheme!.titleLarge!.color!;
|
||||
: Theme.of(context).primaryTextTheme.titleLarge!.color!;
|
||||
|
||||
Color _backgroundColor(BuildContext context) {
|
||||
return Theme.of(context).colorScheme.background;
|
||||
|
@ -89,7 +92,7 @@ class StandardListSeparator extends StatelessWidget {
|
|||
child: Container(
|
||||
height: height,
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme!
|
||||
.primaryTextTheme
|
||||
.titleLarge
|
||||
?.backgroundColor));
|
||||
}
|
||||
|
|
|
@ -5,7 +5,9 @@ import 'package:cake_wallet/entities/cake_2fa_preset_options.dart';
|
|||
import 'package:cake_wallet/entities/exchange_api_mode.dart';
|
||||
import 'package:cake_wallet/entities/pin_code_required_duration.dart';
|
||||
import 'package:cake_wallet/entities/preferences_key.dart';
|
||||
import 'package:cake_wallet/entities/sort_balance_types.dart';
|
||||
import 'package:cake_wallet/utils/device_info.dart';
|
||||
import 'package:cake_wallet/ethereum/ethereum.dart';
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
import 'package:cake_wallet/themes/theme_base.dart';
|
||||
import 'package:cake_wallet/themes/theme_list.dart';
|
||||
|
@ -66,10 +68,14 @@ abstract class SettingsStoreBase with Store {
|
|||
required bool initialShouldRequireTOTP2FAForAddingContacts,
|
||||
required bool initialShouldRequireTOTP2FAForCreatingNewWallets,
|
||||
required bool initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings,
|
||||
required this.sortBalanceBy,
|
||||
required this.pinNativeTokenAtTop,
|
||||
required this.useEtherscan,
|
||||
TransactionPriority? initialBitcoinTransactionPriority,
|
||||
TransactionPriority? initialMoneroTransactionPriority,
|
||||
TransactionPriority? initialHavenTransactionPriority,
|
||||
TransactionPriority? initialLitecoinTransactionPriority})
|
||||
TransactionPriority? initialLitecoinTransactionPriority,
|
||||
TransactionPriority? initialEthereumTransactionPriority})
|
||||
: nodes = ObservableMap<WalletType, Node>.of(nodes),
|
||||
_sharedPreferences = sharedPreferences,
|
||||
fiatCurrency = initialFiatCurrency,
|
||||
|
@ -120,6 +126,10 @@ abstract class SettingsStoreBase with Store {
|
|||
priority[WalletType.litecoin] = initialLitecoinTransactionPriority;
|
||||
}
|
||||
|
||||
if (initialEthereumTransactionPriority != null) {
|
||||
priority[WalletType.ethereum] = initialEthereumTransactionPriority;
|
||||
}
|
||||
|
||||
reaction(
|
||||
(_) => fiatCurrency,
|
||||
(FiatCurrency fiatCurrency) => sharedPreferences.setString(
|
||||
|
@ -145,6 +155,9 @@ abstract class SettingsStoreBase with Store {
|
|||
case WalletType.haven:
|
||||
key = PreferencesKey.havenTransactionPriority;
|
||||
break;
|
||||
case WalletType.ethereum:
|
||||
key = PreferencesKey.ethereumTransactionPriority;
|
||||
break;
|
||||
default:
|
||||
key = null;
|
||||
}
|
||||
|
@ -279,6 +292,21 @@ abstract class SettingsStoreBase with Store {
|
|||
(ExchangeApiMode mode) =>
|
||||
sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, mode.serialize()));
|
||||
|
||||
reaction(
|
||||
(_) => sortBalanceBy,
|
||||
(SortBalanceBy sortBalanceBy) =>
|
||||
_sharedPreferences.setInt(PreferencesKey.sortBalanceBy, sortBalanceBy.index));
|
||||
|
||||
reaction(
|
||||
(_) => pinNativeTokenAtTop,
|
||||
(bool pinNativeTokenAtTop) =>
|
||||
_sharedPreferences.setBool(PreferencesKey.pinNativeTokenAtTop, pinNativeTokenAtTop));
|
||||
|
||||
reaction(
|
||||
(_) => useEtherscan,
|
||||
(bool useEtherscan) =>
|
||||
_sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan));
|
||||
|
||||
this.nodes.observe((change) {
|
||||
if (change.newValue != null && change.key != null) {
|
||||
_saveCurrentNode(change.newValue!, change.key!);
|
||||
|
@ -385,6 +413,15 @@ abstract class SettingsStoreBase with Store {
|
|||
@observable
|
||||
ObservableMap<WalletType, TransactionPriority> priority;
|
||||
|
||||
@observable
|
||||
SortBalanceBy sortBalanceBy;
|
||||
|
||||
@observable
|
||||
bool pinNativeTokenAtTop;
|
||||
|
||||
@observable
|
||||
bool useEtherscan;
|
||||
|
||||
String appVersion;
|
||||
|
||||
String deviceName;
|
||||
|
@ -429,6 +466,7 @@ abstract class SettingsStoreBase with Store {
|
|||
|
||||
TransactionPriority? havenTransactionPriority;
|
||||
TransactionPriority? litecoinTransactionPriority;
|
||||
TransactionPriority? ethereumTransactionPriority;
|
||||
|
||||
if (sharedPreferences.getInt(PreferencesKey.havenTransactionPriority) != null) {
|
||||
havenTransactionPriority = monero?.deserializeMoneroTransactionPriority(
|
||||
|
@ -438,11 +476,16 @@ abstract class SettingsStoreBase with Store {
|
|||
litecoinTransactionPriority = bitcoin?.deserializeLitecoinTransactionPriority(
|
||||
sharedPreferences.getInt(PreferencesKey.litecoinTransactionPriority)!);
|
||||
}
|
||||
if (sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority) != null) {
|
||||
ethereumTransactionPriority = bitcoin?.deserializeLitecoinTransactionPriority(
|
||||
sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority)!);
|
||||
}
|
||||
|
||||
moneroTransactionPriority ??= monero?.getDefaultTransactionPriority();
|
||||
bitcoinTransactionPriority ??= bitcoin?.getMediumTransactionPriority();
|
||||
havenTransactionPriority ??= monero?.getDefaultTransactionPriority();
|
||||
litecoinTransactionPriority ??= bitcoin?.getLitecoinTransactionPriorityMedium();
|
||||
ethereumTransactionPriority ??= ethereum?.getDefaultTransactionPriority();
|
||||
|
||||
final currentBalanceDisplayMode = BalanceDisplayMode.deserialize(
|
||||
raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!);
|
||||
|
@ -502,6 +545,12 @@ abstract class SettingsStoreBase with Store {
|
|||
final pinCodeTimeOutDuration = timeOutDuration != null
|
||||
? PinCodeRequiredDuration.deserialize(raw: timeOutDuration)
|
||||
: defaultPinCodeTimeOutDuration;
|
||||
final sortBalanceBy =
|
||||
SortBalanceBy.values[sharedPreferences.getInt(PreferencesKey.sortBalanceBy) ?? 0];
|
||||
final pinNativeTokenAtTop =
|
||||
sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop) ?? true;
|
||||
final useEtherscan =
|
||||
sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true;
|
||||
|
||||
// If no value
|
||||
if (pinLength == null || pinLength == 0) {
|
||||
|
@ -516,10 +565,12 @@ abstract class SettingsStoreBase with Store {
|
|||
final litecoinElectrumServerId =
|
||||
sharedPreferences.getInt(PreferencesKey.currentLitecoinElectrumSererIdKey);
|
||||
final havenNodeId = sharedPreferences.getInt(PreferencesKey.currentHavenNodeIdKey);
|
||||
final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey);
|
||||
final moneroNode = nodeSource.get(nodeId);
|
||||
final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId);
|
||||
final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId);
|
||||
final havenNode = nodeSource.get(havenNodeId);
|
||||
final ethereumNode = nodeSource.get(ethereumNodeId);
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final deviceName = await _getDeviceName() ?? '';
|
||||
final shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true;
|
||||
|
@ -542,6 +593,10 @@ abstract class SettingsStoreBase with Store {
|
|||
nodes[WalletType.haven] = havenNode;
|
||||
}
|
||||
|
||||
if (ethereumNode != null) {
|
||||
nodes[WalletType.ethereum] = ethereumNode;
|
||||
}
|
||||
|
||||
return SettingsStore(
|
||||
sharedPreferences: sharedPreferences,
|
||||
initialShouldShowMarketPlaceInDashboard: shouldShowMarketPlaceInDashboard,
|
||||
|
@ -567,6 +622,9 @@ abstract class SettingsStoreBase with Store {
|
|||
initialPinLength: pinLength,
|
||||
pinTimeOutDuration: pinCodeTimeOutDuration,
|
||||
initialLanguageCode: savedLanguageCode,
|
||||
sortBalanceBy: sortBalanceBy,
|
||||
pinNativeTokenAtTop: pinNativeTokenAtTop,
|
||||
useEtherscan: useEtherscan,
|
||||
initialMoneroTransactionPriority: moneroTransactionPriority,
|
||||
initialBitcoinTransactionPriority: bitcoinTransactionPriority,
|
||||
initialHavenTransactionPriority: havenTransactionPriority,
|
||||
|
@ -582,6 +640,7 @@ abstract class SettingsStoreBase with Store {
|
|||
initialShouldRequireTOTP2FAForCreatingNewWallets: shouldRequireTOTP2FAForCreatingNewWallets,
|
||||
initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings:
|
||||
shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
|
||||
initialEthereumTransactionPriority: ethereumTransactionPriority,
|
||||
shouldShowYatPopup: shouldShowYatPopup);
|
||||
}
|
||||
|
||||
|
@ -608,6 +667,11 @@ abstract class SettingsStoreBase with Store {
|
|||
sharedPreferences.getInt(PreferencesKey.litecoinTransactionPriority)!) ??
|
||||
priority[WalletType.litecoin]!;
|
||||
}
|
||||
if (sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority) != null) {
|
||||
priority[WalletType.ethereum] = ethereum?.deserializeEthereumTransactionPriority(
|
||||
sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority)!) ??
|
||||
priority[WalletType.ethereum]!;
|
||||
}
|
||||
|
||||
balanceDisplayMode = BalanceDisplayMode.deserialize(
|
||||
raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!);
|
||||
|
@ -677,6 +741,10 @@ abstract class SettingsStoreBase with Store {
|
|||
languageCode = sharedPreferences.getString(PreferencesKey.currentLanguageCode) ?? languageCode;
|
||||
shouldShowYatPopup =
|
||||
sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? shouldShowYatPopup;
|
||||
sortBalanceBy = SortBalanceBy
|
||||
.values[sharedPreferences.getInt(PreferencesKey.sortBalanceBy) ?? sortBalanceBy.index];
|
||||
pinNativeTokenAtTop = sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop) ?? true;
|
||||
useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true;
|
||||
|
||||
final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
|
||||
final bitcoinElectrumServerId =
|
||||
|
@ -684,10 +752,12 @@ abstract class SettingsStoreBase with Store {
|
|||
final litecoinElectrumServerId =
|
||||
sharedPreferences.getInt(PreferencesKey.currentLitecoinElectrumSererIdKey);
|
||||
final havenNodeId = sharedPreferences.getInt(PreferencesKey.currentHavenNodeIdKey);
|
||||
final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey);
|
||||
final moneroNode = nodeSource.get(nodeId);
|
||||
final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId);
|
||||
final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId);
|
||||
final havenNode = nodeSource.get(havenNodeId);
|
||||
final ethereumNode = nodeSource.get(ethereumNodeId);
|
||||
|
||||
if (moneroNode != null) {
|
||||
nodes[WalletType.monero] = moneroNode;
|
||||
|
@ -704,6 +774,10 @@ abstract class SettingsStoreBase with Store {
|
|||
if (havenNode != null) {
|
||||
nodes[WalletType.haven] = havenNode;
|
||||
}
|
||||
|
||||
if (ethereumNode != null) {
|
||||
nodes[WalletType.ethereum] = ethereumNode;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveCurrentNode(Node node, WalletType walletType) async {
|
||||
|
@ -722,6 +796,9 @@ abstract class SettingsStoreBase with Store {
|
|||
case WalletType.haven:
|
||||
await _sharedPreferences.setInt(PreferencesKey.currentHavenNodeIdKey, node.key as int);
|
||||
break;
|
||||
case WalletType.ethereum:
|
||||
await _sharedPreferences.setInt(PreferencesKey.currentEthereumNodeIdKey, node.key as int);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -144,6 +144,7 @@ class ExceptionHandler {
|
|||
"Connection closed before full header was received",
|
||||
"Connection terminated during handshake",
|
||||
"PERMISSION_NOT_GRANTED",
|
||||
"Failed host lookup: ",
|
||||
];
|
||||
|
||||
static Future<void> _addDeviceInfo(File file) async {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'package:cake_wallet/entities/contact_base.dart';
|
||||
import 'package:cake_wallet/entities/wallet_contact.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
|
@ -57,11 +58,15 @@ abstract class ContactListViewModelBase with Store {
|
|||
|
||||
@computed
|
||||
List<ContactRecord> get contactsToShow => contacts
|
||||
.where((element) => _currency == null || element.type == _currency)
|
||||
.where((element) => _isValidForCurrency(element))
|
||||
.toList();
|
||||
|
||||
@computed
|
||||
List<WalletContact> get walletContactsToShow => walletContacts
|
||||
.where((element) => _currency == null || element.type == _currency)
|
||||
.where((element) => _isValidForCurrency(element))
|
||||
.toList();
|
||||
|
||||
bool _isValidForCurrency(ContactBase element) {
|
||||
return _currency == null || element.type == _currency || element.type.title == _currency!.tag;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:cake_wallet/entities/fiat_api_mode.dart';
|
||||
import 'package:cake_wallet/entities/sort_balance_types.dart';
|
||||
import 'package:cw_core/transaction_history.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/balance.dart';
|
||||
|
@ -79,6 +80,15 @@ abstract class BalanceViewModelBase with Store {
|
|||
@computed
|
||||
bool get isFiatDisabled => settingsStore.fiatApiMode == FiatApiMode.disabled;
|
||||
|
||||
@computed
|
||||
bool get isHomeScreenSettingsEnabled => wallet.type == WalletType.ethereum;
|
||||
|
||||
@computed
|
||||
SortBalanceBy get sortBalanceBy => settingsStore.sortBalanceBy;
|
||||
|
||||
@computed
|
||||
bool get pinNativeToken => settingsStore.pinNativeTokenAtTop;
|
||||
|
||||
@computed
|
||||
String get asset {
|
||||
final typeFormatted = walletTypeToString(appStore.wallet!.type);
|
||||
|
@ -109,6 +119,7 @@ abstract class BalanceViewModelBase with Store {
|
|||
switch(wallet.type) {
|
||||
case WalletType.monero:
|
||||
case WalletType.haven:
|
||||
case WalletType.ethereum:
|
||||
return S.current.xmr_available_balance;
|
||||
default:
|
||||
return S.current.confirmed;
|
||||
|
@ -120,6 +131,7 @@ abstract class BalanceViewModelBase with Store {
|
|||
switch(wallet.type) {
|
||||
case WalletType.monero:
|
||||
case WalletType.haven:
|
||||
case WalletType.ethereum:
|
||||
return S.current.xmr_full_balance;
|
||||
default:
|
||||
return S.current.unconfirmed;
|
||||
|
@ -262,11 +274,15 @@ abstract class BalanceViewModelBase with Store {
|
|||
});
|
||||
}
|
||||
|
||||
@computed
|
||||
bool get hasAdditionalBalance => wallet.type != WalletType.ethereum;
|
||||
|
||||
@computed
|
||||
List<BalanceRecord> get formattedBalances {
|
||||
final balance = balances.values.toList();
|
||||
|
||||
balance.sort((BalanceRecord a, BalanceRecord b) {
|
||||
if (wallet.currency == CryptoCurrency.xhv) {
|
||||
if (b.asset == CryptoCurrency.xhv) {
|
||||
return 1;
|
||||
}
|
||||
|
@ -288,6 +304,28 @@ abstract class BalanceViewModelBase with Store {
|
|||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (pinNativeToken) {
|
||||
if (b.asset == wallet.currency) return 1;
|
||||
if (a.asset == wallet.currency) return -1;
|
||||
}
|
||||
|
||||
switch (sortBalanceBy) {
|
||||
case SortBalanceBy.FiatBalance:
|
||||
final aFiatBalance = _getFiatBalance(
|
||||
price: fiatConvertationStore.prices[a.asset] ?? 0, cryptoAmount: a.availableBalance);
|
||||
final bFiatBalance = _getFiatBalance(
|
||||
price: fiatConvertationStore.prices[b.asset] ?? 0, cryptoAmount: b.availableBalance);
|
||||
|
||||
return (double.tryParse(bFiatBalance) ?? 0)
|
||||
.compareTo((double.tryParse(aFiatBalance)) ?? 0);
|
||||
case SortBalanceBy.GrossBalance:
|
||||
return (double.tryParse(b.availableBalance) ?? 0)
|
||||
.compareTo(double.tryParse(a.availableBalance) ?? 0);
|
||||
case SortBalanceBy.Alphabetical:
|
||||
return a.asset.title.compareTo(b.asset.title);
|
||||
}
|
||||
});
|
||||
|
||||
return balance;
|
||||
|
@ -335,7 +373,7 @@ abstract class BalanceViewModelBase with Store {
|
|||
}
|
||||
|
||||
String _getFiatBalance({required double price, String? cryptoAmount}) {
|
||||
if (cryptoAmount == null || cryptoAmount.isEmpty) {
|
||||
if (cryptoAmount == null || cryptoAmount.isEmpty || double.tryParse(cryptoAmount) == null) {
|
||||
return '0.00';
|
||||
}
|
||||
|
||||
|
|
121
lib/view_model/dashboard/home_settings_view_model.dart
Normal file
121
lib/view_model/dashboard/home_settings_view_model.dart
Normal file
|
@ -0,0 +1,121 @@
|
|||
import 'package:cake_wallet/core/fiat_conversion_service.dart';
|
||||
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/store/settings_store.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/erc20_token.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'home_settings_view_model.g.dart';
|
||||
|
||||
class HomeSettingsViewModel = HomeSettingsViewModelBase with _$HomeSettingsViewModel;
|
||||
|
||||
abstract class HomeSettingsViewModelBase with Store {
|
||||
HomeSettingsViewModelBase(this._settingsStore, this._balanceViewModel)
|
||||
: tokens = ObservableSet<Erc20Token>() {
|
||||
_updateTokensList();
|
||||
}
|
||||
|
||||
final SettingsStore _settingsStore;
|
||||
final BalanceViewModel _balanceViewModel;
|
||||
|
||||
final ObservableSet<Erc20Token> tokens;
|
||||
|
||||
@observable
|
||||
String searchText = '';
|
||||
|
||||
@computed
|
||||
SortBalanceBy get sortBalanceBy => _settingsStore.sortBalanceBy;
|
||||
|
||||
@action
|
||||
void setSortBalanceBy(SortBalanceBy value) {
|
||||
_settingsStore.sortBalanceBy = value;
|
||||
_updateTokensList();
|
||||
}
|
||||
|
||||
@computed
|
||||
bool get pinNativeToken => _settingsStore.pinNativeTokenAtTop;
|
||||
|
||||
@action
|
||||
void setPinNativeToken(bool value) => _settingsStore.pinNativeTokenAtTop = value;
|
||||
|
||||
Future<void> addErc20Token(Erc20Token token) async {
|
||||
await ethereum!.addErc20Token(_balanceViewModel.wallet, token);
|
||||
_updateTokensList();
|
||||
_updateFiatPrices(token);
|
||||
}
|
||||
|
||||
Future<void> deleteErc20Token(Erc20Token token) async {
|
||||
await ethereum!.deleteErc20Token(_balanceViewModel.wallet, token);
|
||||
_updateTokensList();
|
||||
}
|
||||
|
||||
Future<Erc20Token?> getErc20Token(String contractAddress) async =>
|
||||
await ethereum!.getErc20Token(_balanceViewModel.wallet, contractAddress);
|
||||
|
||||
CryptoCurrency get nativeToken => _balanceViewModel.wallet.currency;
|
||||
|
||||
void _updateFiatPrices(Erc20Token token) async {
|
||||
try {
|
||||
_balanceViewModel.fiatConvertationStore.prices[token] =
|
||||
await FiatConversionService.fetchPrice(
|
||||
crypto: token,
|
||||
fiat: _settingsStore.fiatCurrency,
|
||||
torOnly: _settingsStore.fiatApiMode == FiatApiMode.torOnly);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void changeTokenAvailability(Erc20Token token, bool value) async {
|
||||
token.enabled = value;
|
||||
ethereum!.addErc20Token(_balanceViewModel.wallet, token);
|
||||
_refreshTokensList();
|
||||
}
|
||||
|
||||
@action
|
||||
void _updateTokensList() {
|
||||
int _sortFunc(Erc20Token e1, Erc20Token e2) {
|
||||
int index1 = _balanceViewModel.formattedBalances.indexWhere((element) => element.asset == e1);
|
||||
int index2 = _balanceViewModel.formattedBalances.indexWhere((element) => element.asset == e2);
|
||||
|
||||
if (e1.enabled && !e2.enabled) {
|
||||
return -1;
|
||||
} else if (e2.enabled && !e1.enabled) {
|
||||
return 1;
|
||||
} else if (!e1.enabled && !e2.enabled) { // if both are disabled then sort alphabetically
|
||||
return e1.name.compareTo(e2.name);
|
||||
}
|
||||
|
||||
return index1.compareTo(index2);
|
||||
}
|
||||
|
||||
tokens.clear();
|
||||
|
||||
tokens.addAll(ethereum!
|
||||
.getERC20Currencies(_balanceViewModel.wallet)
|
||||
.where((element) => _matchesSearchText(element))
|
||||
.toList()
|
||||
..sort(_sortFunc));
|
||||
}
|
||||
|
||||
@action
|
||||
void _refreshTokensList() {
|
||||
final _tokens = Set.of(tokens);
|
||||
tokens.clear();
|
||||
tokens.addAll(_tokens);
|
||||
}
|
||||
|
||||
@action
|
||||
void changeSearchText(String text) {
|
||||
searchText = text;
|
||||
_updateTokensList();
|
||||
}
|
||||
|
||||
bool _matchesSearchText(Erc20Token asset) {
|
||||
return searchText.isEmpty ||
|
||||
asset.fullName!.toLowerCase().contains(searchText.toLowerCase()) ||
|
||||
asset.title.toLowerCase().contains(searchText.toLowerCase()) ||
|
||||
asset.contractAddress == searchText;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:cake_wallet/entities/balance_display_mode.dart';
|
||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||
import 'package:cake_wallet/ethereum/ethereum.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cw_core/transaction_direction.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
|
@ -84,6 +85,13 @@ class TransactionListItem extends ActionListItem with Keyable {
|
|||
cryptoAmount: haven!.formatterMoneroAmountToDouble(amount: transaction.amount),
|
||||
price: price);
|
||||
break;
|
||||
case WalletType.ethereum:
|
||||
final asset = ethereum!.assetOfTransaction(balanceViewModel.wallet, transaction);
|
||||
final price = balanceViewModel.fiatConvertationStore.prices[asset];
|
||||
amount = calculateFiatAmountRaw(
|
||||
cryptoAmount: ethereum!.formatterEthereumAmountToDouble(transaction: transaction),
|
||||
price: price);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -699,6 +699,10 @@ abstract class ExchangeViewModelBase with Store {
|
|||
depositCurrency = CryptoCurrency.xhv;
|
||||
receiveCurrency = CryptoCurrency.btc;
|
||||
break;
|
||||
case WalletType.ethereum:
|
||||
depositCurrency = CryptoCurrency.eth;
|
||||
receiveCurrency = CryptoCurrency.xmr;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -63,6 +63,9 @@ abstract class NodeListViewModelBase with Store {
|
|||
case WalletType.haven:
|
||||
node = getHavenDefaultNode(nodes: _nodeSource)!;
|
||||
break;
|
||||
case WalletType.ethereum:
|
||||
node = getEthereumDefaultNode(nodes: _nodeSource)!;
|
||||
break;
|
||||
default:
|
||||
throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}');
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:cake_wallet/di.dart';
|
|||
import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart';
|
||||
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
|
||||
import 'package:cake_wallet/entities/parsed_address.dart';
|
||||
import 'package:cake_wallet/ethereum/ethereum.dart';
|
||||
import 'package:cake_wallet/haven/haven.dart';
|
||||
import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
|
@ -90,6 +91,9 @@ abstract class OutputBase with Store {
|
|||
case WalletType.haven:
|
||||
_amount = haven!.formatterMoneroParseAmount(amount: _cryptoAmount);
|
||||
break;
|
||||
case WalletType.ethereum:
|
||||
_amount = ethereum!.formatterEthereumParseAmount(_cryptoAmount);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -123,6 +127,10 @@ abstract class OutputBase with Store {
|
|||
if (_wallet.type == WalletType.haven) {
|
||||
return haven!.formatterMoneroAmountToDouble(amount: fee);
|
||||
}
|
||||
|
||||
if (_wallet.type == WalletType.ethereum) {
|
||||
return ethereum!.formatterEthereumAmountToDouble(amount: BigInt.from(fee));
|
||||
}
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
}
|
||||
|
@ -133,8 +141,9 @@ abstract class OutputBase with Store {
|
|||
@computed
|
||||
String get estimatedFeeFiatAmount {
|
||||
try {
|
||||
final currency = _wallet.type == WalletType.ethereum ? _wallet.currency : cryptoCurrencyHandler();
|
||||
final fiat = calculateFiatAmountRaw(
|
||||
price: _fiatConversationStore.prices[cryptoCurrencyHandler()]!,
|
||||
price: _fiatConversationStore.prices[currency]!,
|
||||
cryptoAmount: estimatedFee);
|
||||
return fiat;
|
||||
} catch (_) {
|
||||
|
@ -228,6 +237,9 @@ abstract class OutputBase with Store {
|
|||
case WalletType.haven:
|
||||
maximumFractionDigits = 12;
|
||||
break;
|
||||
case WalletType.ethereum:
|
||||
maximumFractionDigits = 12;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -13,8 +13,7 @@ import 'package:cake_wallet/store/settings_store.dart';
|
|||
|
||||
part 'send_template_view_model.g.dart';
|
||||
|
||||
class SendTemplateViewModel = SendTemplateViewModelBase
|
||||
with _$SendTemplateViewModel;
|
||||
class SendTemplateViewModel = SendTemplateViewModelBase with _$SendTemplateViewModel;
|
||||
|
||||
abstract class SendTemplateViewModelBase with Store {
|
||||
final WalletBase _wallet;
|
||||
|
@ -22,8 +21,8 @@ abstract class SendTemplateViewModelBase with Store {
|
|||
final SendTemplateStore _sendTemplateStore;
|
||||
final FiatConversionStore _fiatConversationStore;
|
||||
|
||||
SendTemplateViewModelBase(this._wallet, this._settingsStore,
|
||||
this._sendTemplateStore, this._fiatConversationStore)
|
||||
SendTemplateViewModelBase(
|
||||
this._wallet, this._settingsStore, this._sendTemplateStore, this._fiatConversationStore)
|
||||
: recipients = ObservableList<TemplateViewModel>() {
|
||||
addRecipient();
|
||||
}
|
||||
|
@ -33,7 +32,6 @@ abstract class SendTemplateViewModelBase with Store {
|
|||
@action
|
||||
void addRecipient() {
|
||||
recipients.add(TemplateViewModel(
|
||||
cryptoCurrency: cryptoCurrency,
|
||||
wallet: _wallet,
|
||||
settingsStore: _settingsStore,
|
||||
fiatConversationStore: _fiatConversationStore));
|
||||
|
@ -47,11 +45,13 @@ abstract class SendTemplateViewModelBase with Store {
|
|||
AmountValidator get amountValidator =>
|
||||
AmountValidator(currency: walletTypeToCryptoCurrency(_wallet.type));
|
||||
|
||||
AddressValidator get addressValidator =>
|
||||
AddressValidator(type: _wallet.currency);
|
||||
AddressValidator get addressValidator => AddressValidator(type: _wallet.currency);
|
||||
|
||||
TemplateValidator get templateValidator => TemplateValidator();
|
||||
|
||||
bool get hasMultiRecipient =>
|
||||
_wallet.type != WalletType.haven && _wallet.type != WalletType.ethereum;
|
||||
|
||||
@computed
|
||||
CryptoCurrency get cryptoCurrency => _wallet.currency;
|
||||
|
||||
|
@ -68,6 +68,7 @@ abstract class SendTemplateViewModelBase with Store {
|
|||
void addTemplate(
|
||||
{required String name,
|
||||
required bool isCurrencySelected,
|
||||
required String cryptoCurrency,
|
||||
required String address,
|
||||
required String amount,
|
||||
required String amountFiat,
|
||||
|
@ -76,7 +77,7 @@ abstract class SendTemplateViewModelBase with Store {
|
|||
name: name,
|
||||
isCurrencySelected: isCurrencySelected,
|
||||
address: address,
|
||||
cryptoCurrency: cryptoCurrency.title,
|
||||
cryptoCurrency: cryptoCurrency,
|
||||
fiatCurrency: fiatCurrency,
|
||||
amount: amount,
|
||||
amountFiat: amountFiat,
|
||||
|
@ -89,4 +90,7 @@ abstract class SendTemplateViewModelBase with Store {
|
|||
_sendTemplateStore.remove(template: template);
|
||||
updateTemplate();
|
||||
}
|
||||
|
||||
@computed
|
||||
List<CryptoCurrency> get walletCurrencies => _wallet.balance.keys.toList();
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
|
|||
import 'package:cake_wallet/entities/transaction_description.dart';
|
||||
import 'package:cake_wallet/entities/wallet_contact.dart';
|
||||
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
|
||||
import 'package:cake_wallet/ethereum/ethereum.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
import 'package:cake_wallet/view_model/send/output.dart';
|
||||
|
@ -45,6 +46,7 @@ abstract class SendViewModelBase with Store {
|
|||
: state = InitialExecutionState(),
|
||||
currencies = _wallet.balance.keys.toList(),
|
||||
selectedCryptoCurrency = _wallet.currency,
|
||||
hasMultipleTokens = _wallet.type == WalletType.ethereum,
|
||||
outputs = ObservableList<Output>(),
|
||||
fiatFromSettings = _settingsStore.fiatCurrency {
|
||||
final priority = _settingsStore.priority[_wallet.type];
|
||||
|
@ -105,8 +107,11 @@ abstract class SendViewModelBase with Store {
|
|||
String get pendingTransactionFeeFiatAmount {
|
||||
try {
|
||||
if (pendingTransaction != null) {
|
||||
final currency = walletType == WalletType.ethereum
|
||||
? _wallet.currency
|
||||
: selectedCryptoCurrency;
|
||||
final fiat = calculateFiatAmount(
|
||||
price: _fiatConversationStore.prices[selectedCryptoCurrency]!,
|
||||
price: _fiatConversationStore.prices[currency]!,
|
||||
cryptoAmount: pendingTransaction!.feeFormatted);
|
||||
return fiat;
|
||||
} else {
|
||||
|
@ -131,14 +136,14 @@ abstract class SendViewModelBase with Store {
|
|||
|
||||
CryptoCurrency get currency => _wallet.currency;
|
||||
|
||||
Validator get amountValidator =>
|
||||
Validator<String> get amountValidator =>
|
||||
AmountValidator(currency: walletTypeToCryptoCurrency(_wallet.type));
|
||||
|
||||
Validator get allAmountValidator => AllAmountValidator();
|
||||
Validator<String> get allAmountValidator => AllAmountValidator();
|
||||
|
||||
Validator get addressValidator => AddressValidator(type: selectedCryptoCurrency);
|
||||
Validator<String> get addressValidator => AddressValidator(type: selectedCryptoCurrency);
|
||||
|
||||
Validator get textValidator => TextValidator();
|
||||
Validator<String> get textValidator => TextValidator();
|
||||
|
||||
final FiatCurrency fiatFromSettings;
|
||||
|
||||
|
@ -146,7 +151,7 @@ abstract class SendViewModelBase with Store {
|
|||
PendingTransaction? pendingTransaction;
|
||||
|
||||
@computed
|
||||
String get balance => balanceViewModel.availableBalance;
|
||||
String get balance => _wallet.balance[selectedCryptoCurrency]!.formattedAvailableBalance;
|
||||
|
||||
@computed
|
||||
bool get isFiatDisabled => balanceViewModel.isFiatDisabled;
|
||||
|
@ -176,10 +181,9 @@ abstract class SendViewModelBase with Store {
|
|||
|
||||
List<CryptoCurrency> currencies;
|
||||
|
||||
bool get hasMultiRecipient => _wallet.type != WalletType.haven;
|
||||
|
||||
bool get hasYat => outputs
|
||||
.any((out) => out.isParsedAddress && out.parsedAddress.parseFrom == ParseFrom.yatRecord);
|
||||
bool get hasYat => outputs.any((out) =>
|
||||
out.isParsedAddress &&
|
||||
out.parsedAddress.parseFrom == ParseFrom.yatRecord);
|
||||
|
||||
WalletType get walletType => _wallet.type;
|
||||
|
||||
|
@ -198,6 +202,7 @@ abstract class SendViewModelBase with Store {
|
|||
final ContactListViewModel contactListViewModel;
|
||||
final FiatConversionStore _fiatConversationStore;
|
||||
final Box<TransactionDescription> transactionDescriptionBox;
|
||||
final bool hasMultipleTokens;
|
||||
|
||||
@computed
|
||||
List<ContactRecord> get contactsToShow => contactListViewModel.contacts
|
||||
|
@ -351,6 +356,15 @@ abstract class SendViewModelBase with Store {
|
|||
|
||||
return haven!.createHavenTransactionCreationCredentials(
|
||||
outputs: outputs, priority: priority, assetType: selectedCryptoCurrency.title);
|
||||
case WalletType.ethereum:
|
||||
final priority = _settingsStore.priority[_wallet.type];
|
||||
|
||||
if (priority == null) {
|
||||
throw Exception('Priority is null for wallet type: ${_wallet.type}');
|
||||
}
|
||||
|
||||
return ethereum!.createEthereumTransactionCredentials(
|
||||
outputs, priority: priority, currency: selectedCryptoCurrency);
|
||||
default:
|
||||
throw Exception('Unexpected wallet type: ${_wallet.type}');
|
||||
}
|
||||
|
@ -369,11 +383,22 @@ abstract class SendViewModelBase with Store {
|
|||
}
|
||||
|
||||
bool _isEqualCurrency(String currency) =>
|
||||
currency.toLowerCase() == _wallet.currency.title.toLowerCase();
|
||||
_wallet.balance.keys.any((e) => currency.toLowerCase() == e.title.toLowerCase());
|
||||
|
||||
@action
|
||||
void onClose() => _settingsStore.fiatCurrency = fiatFromSettings;
|
||||
|
||||
@action
|
||||
void setFiatCurrency(FiatCurrency fiat) => _settingsStore.fiatCurrency = fiat;
|
||||
void setFiatCurrency(FiatCurrency fiat) =>
|
||||
_settingsStore.fiatCurrency = fiat;
|
||||
|
||||
@action
|
||||
void setSelectedCryptoCurrency(String cryptoCurrency) {
|
||||
try {
|
||||
selectedCryptoCurrency = _wallet.balance.keys
|
||||
.firstWhere((e) => cryptoCurrency.toLowerCase() == e.title.toLowerCase());
|
||||
} catch (e) {
|
||||
selectedCryptoCurrency = _wallet.currency;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,23 +11,20 @@ part 'template_view_model.g.dart';
|
|||
class TemplateViewModel = TemplateViewModelBase with _$TemplateViewModel;
|
||||
|
||||
abstract class TemplateViewModelBase with Store {
|
||||
final CryptoCurrency cryptoCurrency;
|
||||
final WalletBase _wallet;
|
||||
final SettingsStore _settingsStore;
|
||||
final FiatConversionStore _fiatConversationStore;
|
||||
|
||||
TemplateViewModelBase(
|
||||
{required this.cryptoCurrency,
|
||||
TemplateViewModelBase({
|
||||
required WalletBase wallet,
|
||||
required SettingsStore settingsStore,
|
||||
required FiatConversionStore fiatConversationStore})
|
||||
: _wallet = wallet,
|
||||
required FiatConversionStore fiatConversationStore,
|
||||
}) : _wallet = wallet,
|
||||
_settingsStore = settingsStore,
|
||||
_fiatConversationStore = fiatConversationStore,
|
||||
output = Output(wallet, settingsStore, fiatConversationStore,
|
||||
() => wallet.currency) {
|
||||
output = Output(
|
||||
_wallet, _settingsStore, _fiatConversationStore, () => cryptoCurrency);
|
||||
_currency = wallet.currency,
|
||||
output = Output(wallet, settingsStore, fiatConversationStore, () => wallet.currency) {
|
||||
output = Output(_wallet, _settingsStore, _fiatConversationStore, () => _currency);
|
||||
}
|
||||
|
||||
@observable
|
||||
|
@ -39,6 +36,9 @@ abstract class TemplateViewModelBase with Store {
|
|||
@observable
|
||||
String address = '';
|
||||
|
||||
@observable
|
||||
CryptoCurrency _currency;
|
||||
|
||||
@observable
|
||||
bool isCurrencySelected = true;
|
||||
|
||||
|
@ -66,8 +66,7 @@ abstract class TemplateViewModelBase with Store {
|
|||
output.reset();
|
||||
}
|
||||
|
||||
Template toTemplate(
|
||||
{required String cryptoCurrency, required String fiatCurrency}) {
|
||||
Template toTemplate({required String cryptoCurrency, required String fiatCurrency}) {
|
||||
return Template(
|
||||
isCurrencySelectedRaw: isCurrencySelected,
|
||||
nameRaw: name,
|
||||
|
@ -77,4 +76,13 @@ abstract class TemplateViewModelBase with Store {
|
|||
amountRaw: output.cryptoAmount,
|
||||
amountFiatRaw: output.fiatAmount);
|
||||
}
|
||||
|
||||
@action
|
||||
void changeSelectedCurrency(CryptoCurrency currency) {
|
||||
isCurrencySelected = true;
|
||||
_currency = currency;
|
||||
}
|
||||
|
||||
@computed
|
||||
CryptoCurrency get selectedCurrency => _currency;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import 'package:cake_wallet/entities/exchange_api_mode.dart';
|
||||
import 'package:cake_wallet/ethereum/ethereum.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:cake_wallet/entities/fiat_api_mode.dart';
|
||||
|
||||
|
@ -8,9 +11,10 @@ part 'privacy_settings_view_model.g.dart';
|
|||
class PrivacySettingsViewModel = PrivacySettingsViewModelBase with _$PrivacySettingsViewModel;
|
||||
|
||||
abstract class PrivacySettingsViewModelBase with Store {
|
||||
PrivacySettingsViewModelBase(this._settingsStore);
|
||||
PrivacySettingsViewModelBase(this._settingsStore, this._wallet);
|
||||
|
||||
final SettingsStore _settingsStore;
|
||||
final WalletBase _wallet;
|
||||
|
||||
@computed
|
||||
ExchangeApiMode get exchangeStatus => _settingsStore.exchangeStatus;
|
||||
|
@ -30,8 +34,14 @@ abstract class PrivacySettingsViewModelBase with Store {
|
|||
@computed
|
||||
bool get disableSell => _settingsStore.disableSell;
|
||||
|
||||
@computed
|
||||
bool get useEtherscan => _settingsStore.useEtherscan;
|
||||
|
||||
bool get canUseEtherscan => _wallet.type == WalletType.ethereum;
|
||||
|
||||
@action
|
||||
void setShouldSaveRecipientAddress(bool value) => _settingsStore.shouldSaveRecipientAddress = value;
|
||||
void setShouldSaveRecipientAddress(bool value) =>
|
||||
_settingsStore.shouldSaveRecipientAddress = value;
|
||||
|
||||
@action
|
||||
void setExchangeApiMode(ExchangeApiMode value) => _settingsStore.exchangeStatus = value;
|
||||
|
@ -48,4 +58,9 @@ abstract class PrivacySettingsViewModelBase with Store {
|
|||
@action
|
||||
void setDisableSell(bool value) => _settingsStore.disableSell = value;
|
||||
|
||||
@action
|
||||
void setUseEtherscan(bool value) {
|
||||
_settingsStore.useEtherscan = value;
|
||||
ethereum!.updateEtherscanUsageState(_wallet, value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'package:cw_core/transaction_direction.dart';
|
|||
import 'package:cake_wallet/utils/date_formatter.dart';
|
||||
import 'package:cake_wallet/entities/transaction_description.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:intl/src/intl/date_format.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
|
@ -32,100 +33,22 @@ abstract class TransactionDetailsViewModelBase with Store {
|
|||
final dateFormat = DateFormatter.withCurrentLocal();
|
||||
final tx = transactionInfo;
|
||||
|
||||
if (wallet.type == WalletType.monero) {
|
||||
final key = tx.additionalInfo['key'] as String?;
|
||||
final accountIndex = tx.additionalInfo['accountIndex'] as int;
|
||||
final addressIndex = tx.additionalInfo['addressIndex'] as int;
|
||||
final feeFormatted = tx.feeFormatted();
|
||||
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_height, value: '${tx.height}'),
|
||||
StandartListItem(
|
||||
title: S.current.transaction_details_amount,
|
||||
value: tx.amountFormatted()),
|
||||
if (feeFormatted != null)
|
||||
StandartListItem(
|
||||
title: S.current.transaction_details_fee, value: feeFormatted),
|
||||
if (key?.isNotEmpty ?? false)
|
||||
StandartListItem(title: S.current.transaction_key, value: key!)
|
||||
];
|
||||
|
||||
if (tx.direction == TransactionDirection.incoming &&
|
||||
accountIndex != null &&
|
||||
addressIndex != null) {
|
||||
try {
|
||||
final address = monero!.getTransactionAddress(wallet, accountIndex, addressIndex);
|
||||
final label = monero!.getSubaddressLabel(wallet, accountIndex, addressIndex);
|
||||
|
||||
if (address?.isNotEmpty ?? false) {
|
||||
isRecipientAddressShown = true;
|
||||
_items.add(
|
||||
StandartListItem(
|
||||
title: S.current.transaction_details_recipient_address,
|
||||
value: address));
|
||||
}
|
||||
|
||||
if (label?.isNotEmpty ?? false) {
|
||||
_items.add(
|
||||
StandartListItem(
|
||||
title: S.current.address_label,
|
||||
value: label)
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
items.addAll(_items);
|
||||
}
|
||||
|
||||
if (wallet.type == WalletType.bitcoin
|
||||
|| wallet.type == WalletType.litecoin) {
|
||||
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.confirmations,
|
||||
value: tx.confirmations.toString()),
|
||||
StandartListItem(
|
||||
title: S.current.transaction_details_height, value: '${tx.height}'),
|
||||
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()!),
|
||||
];
|
||||
|
||||
items.addAll(_items);
|
||||
}
|
||||
|
||||
if (wallet.type == WalletType.haven) {
|
||||
items.addAll([
|
||||
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_height, value: '${tx.height}'),
|
||||
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()!),
|
||||
]);
|
||||
switch (wallet.type) {
|
||||
case WalletType.monero:
|
||||
_addMoneroListItems(tx, dateFormat);
|
||||
break;
|
||||
case WalletType.bitcoin:
|
||||
case WalletType.litecoin:
|
||||
_addElectrumListItems(tx, dateFormat);
|
||||
break;
|
||||
case WalletType.haven:
|
||||
_addHavenListItems(tx, dateFormat);
|
||||
break;
|
||||
case WalletType.ethereum:
|
||||
_addEthereumListItems(tx, dateFormat);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (showRecipientAddress && !isRecipientAddressShown) {
|
||||
|
@ -136,8 +59,7 @@ abstract class TransactionDetailsViewModelBase with Store {
|
|||
|
||||
if (recipientAddress?.isNotEmpty ?? false) {
|
||||
items.add(StandartListItem(
|
||||
title: S.current.transaction_details_recipient_address,
|
||||
value: recipientAddress!));
|
||||
title: S.current.transaction_details_recipient_address, value: recipientAddress!));
|
||||
}
|
||||
} catch (_) {
|
||||
// FIX-ME: Unhandled exception
|
||||
|
@ -192,6 +114,8 @@ abstract class TransactionDetailsViewModelBase with Store {
|
|||
return 'https://blockchair.com/litecoin/transaction/${txId}';
|
||||
case WalletType.haven:
|
||||
return 'https://explorer.havenprotocol.org/search?value=${txId}';
|
||||
case WalletType.ethereum:
|
||||
return 'https://etherscan.io/tx/${txId}';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
@ -207,8 +131,92 @@ abstract class TransactionDetailsViewModelBase with Store {
|
|||
return S.current.view_transaction_on + 'Blockchair.com';
|
||||
case WalletType.haven:
|
||||
return S.current.view_transaction_on + 'explorer.havenprotocol.org';
|
||||
case WalletType.ethereum:
|
||||
return S.current.view_transaction_on + 'etherscan.io';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
void _addMoneroListItems(TransactionInfo tx, DateFormat dateFormat) {
|
||||
final key = tx.additionalInfo['key'] as String?;
|
||||
final accountIndex = tx.additionalInfo['accountIndex'] as int;
|
||||
final addressIndex = tx.additionalInfo['addressIndex'] as int;
|
||||
final feeFormatted = tx.feeFormatted();
|
||||
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_height, value: '${tx.height}'),
|
||||
StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()),
|
||||
if (feeFormatted != null)
|
||||
StandartListItem(title: S.current.transaction_details_fee, value: feeFormatted),
|
||||
if (key?.isNotEmpty ?? false) StandartListItem(title: S.current.transaction_key, value: key!),
|
||||
];
|
||||
|
||||
if (tx.direction == TransactionDirection.incoming) {
|
||||
try {
|
||||
final address = monero!.getTransactionAddress(wallet, accountIndex, addressIndex);
|
||||
final label = monero!.getSubaddressLabel(wallet, accountIndex, addressIndex);
|
||||
|
||||
if (address.isNotEmpty) {
|
||||
isRecipientAddressShown = true;
|
||||
_items.add(StandartListItem(
|
||||
title: S.current.transaction_details_recipient_address,
|
||||
value: address,
|
||||
));
|
||||
}
|
||||
|
||||
if (label.isNotEmpty) {
|
||||
_items.add(StandartListItem(title: S.current.address_label, value: label));
|
||||
}
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
items.addAll(_items);
|
||||
}
|
||||
|
||||
void _addElectrumListItems(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.confirmations, value: tx.confirmations.toString()),
|
||||
StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'),
|
||||
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()!),
|
||||
];
|
||||
|
||||
items.addAll(_items);
|
||||
}
|
||||
|
||||
void _addHavenListItems(TransactionInfo tx, DateFormat dateFormat) {
|
||||
items.addAll([
|
||||
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_height, value: '${tx.height}'),
|
||||
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()!),
|
||||
]);
|
||||
}
|
||||
|
||||
void _addEthereumListItems(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.confirmations, value: tx.confirmations.toString()),
|
||||
StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'),
|
||||
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()!),
|
||||
];
|
||||
|
||||
items.addAll(_items);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:cake_wallet/ethereum/ethereum.dart';
|
||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
|
||||
import 'package:cake_wallet/store/yat/yat_store.dart';
|
||||
|
@ -93,6 +94,22 @@ class LitecoinURI extends PaymentURI {
|
|||
}
|
||||
}
|
||||
|
||||
class EthereumURI extends PaymentURI {
|
||||
EthereumURI({required String amount, required String address})
|
||||
: super(amount: amount, address: address);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var base = 'ethereum:' + address;
|
||||
|
||||
if (amount.isNotEmpty) {
|
||||
base += '?amount=${amount.replaceAll(',', '.')}';
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class WalletAddressListViewModelBase with Store {
|
||||
WalletAddressListViewModelBase({
|
||||
required AppStore appStore,
|
||||
|
@ -151,6 +168,10 @@ abstract class WalletAddressListViewModelBase with Store {
|
|||
return LitecoinURI(amount: amount, address: address.address);
|
||||
}
|
||||
|
||||
if (_wallet.type == WalletType.ethereum) {
|
||||
return EthereumURI(amount: amount, address: address.address);
|
||||
}
|
||||
|
||||
throw Exception('Unexpected type: ${type.toString()}');
|
||||
}
|
||||
|
||||
|
@ -202,6 +223,12 @@ abstract class WalletAddressListViewModelBase with Store {
|
|||
addressList.addAll(bitcoinAddresses);
|
||||
}
|
||||
|
||||
if (wallet.type == WalletType.ethereum) {
|
||||
final primaryAddress = ethereum!.getAddress(wallet);
|
||||
|
||||
addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress));
|
||||
}
|
||||
|
||||
return addressList;
|
||||
}
|
||||
|
||||
|
@ -226,6 +253,10 @@ abstract class WalletAddressListViewModelBase with Store {
|
|||
@computed
|
||||
bool get hasAddressList => _wallet.type == WalletType.monero || _wallet.type == WalletType.haven;
|
||||
|
||||
@computed
|
||||
bool get showElectrumAddressDisclaimer =>
|
||||
_wallet.type == WalletType.bitcoin || _wallet.type == WalletType.litecoin;
|
||||
|
||||
@observable
|
||||
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> _wallet;
|
||||
|
||||
|
|
|
@ -17,7 +17,8 @@ class WalletKeysViewModel = WalletKeysViewModelBase with _$WalletKeysViewModel;
|
|||
abstract class WalletKeysViewModelBase with Store {
|
||||
WalletKeysViewModelBase(this._appStore)
|
||||
: title = _appStore.wallet!.type == WalletType.bitcoin ||
|
||||
_appStore.wallet!.type == WalletType.litecoin
|
||||
_appStore.wallet!.type == WalletType.litecoin ||
|
||||
_appStore.wallet!.type == WalletType.ethereum
|
||||
? S.current.wallet_seed
|
||||
: S.current.wallet_keys,
|
||||
_restoreHeight = _appStore.wallet!.walletInfo.restoreHeight,
|
||||
|
@ -89,7 +90,8 @@ abstract class WalletKeysViewModelBase with Store {
|
|||
}
|
||||
|
||||
if (_appStore.wallet!.type == WalletType.bitcoin ||
|
||||
_appStore.wallet!.type == WalletType.litecoin) {
|
||||
_appStore.wallet!.type == WalletType.litecoin ||
|
||||
_appStore.wallet!.type == WalletType.ethereum) {
|
||||
items.addAll([
|
||||
StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed),
|
||||
]);
|
||||
|
@ -116,6 +118,8 @@ abstract class WalletKeysViewModelBase with Store {
|
|||
return 'litecoin-wallet';
|
||||
case WalletType.haven:
|
||||
return 'haven-wallet';
|
||||
case WalletType.ethereum:
|
||||
return 'ethereum-wallet';
|
||||
default:
|
||||
throw Exception('Unexpected wallet type: ${_appStore.wallet!.toString()}');
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue