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:
Omar Hatem 2023-08-04 20:01:49 +03:00 committed by GitHub
parent 4120394121
commit 3ce4000dcf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
137 changed files with 7164 additions and 1492 deletions

View file

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

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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

View 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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -75,4 +75,6 @@ abstract class WalletBase<
Future<void>? updateBalance();
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => null;
Future<void> renameWalletFiles(String newWalletName);
}

View file

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

View file

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

@ -0,0 +1,30 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/

10
cw_ethereum/.metadata Normal file
View file

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: eb6d86ee27deecba4a83536aa20f366a6044895c
channel: stable
project_type: package

3
cw_ethereum/CHANGELOG.md Normal file
View file

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

1
cw_ethereum/LICENSE Normal file
View file

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

39
cw_ethereum/README.md Normal file
View file

@ -0,0 +1,39 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/developing-packages).
-->
TODO: Put a short description of the package here that helps potential users
know whether this package might be useful for them.
## Features
TODO: List what your package can do. Maybe include images, gifs, or videos.
## Getting started
TODO: List prerequisites and provide or point to information on how to
start using the package.
## Usage
TODO: Include short and useful examples for package users. Add longer examples
to `/example` folder.
```dart
const like = 'sample';
```
## Additional information
TODO: Tell users more about the package: where to find more information, how to
contribute to the package, how to file issues, what response they can expect
from the package authors, and more.

View file

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View file

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

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

View 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);
}
}
}

View 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;
// }
}

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

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

File diff suppressed because it is too large Load diff

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

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

View 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,
};
}

View 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",
);
}

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

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

View file

@ -0,0 +1,33 @@
import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:mobx/mobx.dart';
part '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());
}
}
}

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

View 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
View file

@ -0,0 +1,39 @@
import 'dart:io';
import 'package:cw_core/key.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
Future<void> write(
{required String path,
required String password,
required String data}) async {
final keys = extractKeys(password);
final key = encrypt.Key.fromBase64(keys.first);
final iv = encrypt.IV.fromBase64(keys.last);
final encrypted = await encode(key: key, iv: iv, data: data);
final f = File(path);
f.writeAsStringSync(encrypted);
}
Future<void> writeData(
{required String path,
required String password,
required String data}) async {
final keys = extractKeys(password);
final key = encrypt.Key.fromBase64(keys.first);
final iv = encrypt.IV.fromBase64(keys.last);
final encrypted = await encode(key: key, iv: iv, data: data);
final f = File(path);
f.writeAsStringSync(encrypted);
}
Future<String> read({required String path, required String password}) async {
final file = File(path);
if (!file.existsSync()) {
file.createSync();
}
final encrypted = file.readAsStringSync();
return decode(password: password, data: encrypted);
}

View file

@ -0,0 +1,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
View 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

View 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);
});
}

View file

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

View file

@ -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(),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -55,5 +55,5 @@ class Template extends HiveObject {
String get amount => amountRaw ?? '';
List<Template>? get additionalRecipients => additionalRecipientsRaw ?? null;
List<Template>? get additionalRecipients => additionalRecipientsRaw;
}

View 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);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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),
],
),
);
}
}

View 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),
),
);
},
),
);
},
),
),
),
],
),
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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!,
),
),
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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