This commit is contained in:
M 2020-08-25 19:32:40 +03:00
parent 4572049c5d
commit 5eefd6a31b
35 changed files with 1273 additions and 618 deletions

4
.gitignore vendored
View file

@ -89,4 +89,6 @@ android/key.properties
**/tool/.secrets-prod.json **/tool/.secrets-prod.json
**/lib/.secrets.g.dart **/lib/.secrets.g.dart
vendor/ vendor/
android/app/.cxx/**

View file

@ -373,7 +373,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_TEAM = 32J6BB6VUS; DEVELOPMENT_TEAM = 32J6BB6VUS;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@ -387,7 +387,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
MARKETING_VERSION = 3.1.28; MARKETING_VERSION = 3.2.0;
PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cakewallet; PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cakewallet;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@ -509,7 +509,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_TEAM = 32J6BB6VUS; DEVELOPMENT_TEAM = 32J6BB6VUS;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@ -523,7 +523,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
MARKETING_VERSION = 3.1.28; MARKETING_VERSION = 3.2.0;
PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cakewallet; PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cakewallet;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@ -540,7 +540,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = 6;
DEVELOPMENT_TEAM = 32J6BB6VUS; DEVELOPMENT_TEAM = 32J6BB6VUS;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@ -554,7 +554,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
MARKETING_VERSION = 3.1.28; MARKETING_VERSION = 3.2.0;
PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cakewallet; PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cakewallet;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";

View file

@ -19,7 +19,7 @@
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>

View file

@ -1,17 +1,19 @@
import 'dart:convert'; import 'dart:convert';
class BitcoinAddressRecord { class BitcoinAddressRecord {
BitcoinAddressRecord(this.address, {this.label}); BitcoinAddressRecord(this.address, {this.label, this.index});
factory BitcoinAddressRecord.fromJSON(String jsonSource) { factory BitcoinAddressRecord.fromJSON(String jsonSource) {
final decoded = json.decode(jsonSource) as Map; final decoded = json.decode(jsonSource) as Map;
return BitcoinAddressRecord(decoded['address'] as String, return BitcoinAddressRecord(decoded['address'] as String,
label: decoded['label'] as String); label: decoded['label'] as String, index: decoded['index'] as int);
} }
final String address; final String address;
int index;
String label; String label;
String toJSON() => json.encode({'label': label, 'address': address}); String toJSON() =>
json.encode({'label': label, 'address': address, 'index': index});
} }

View file

@ -1,6 +1,9 @@
import 'package:cake_wallet/src/domain/common/transaction_priority.dart';
class BitcoinTransactionCredentials { class BitcoinTransactionCredentials {
const BitcoinTransactionCredentials(this.address, this.amount); BitcoinTransactionCredentials(this.address, this.amount, this.priority);
final String address; final String address;
final double amount; final double amount;
TransactionPriority priority;
} }

View file

@ -6,8 +6,6 @@ import 'package:cake_wallet/bitcoin/file.dart';
import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart';
import 'package:cake_wallet/bitcoin/electrum.dart'; import 'package:cake_wallet/bitcoin/electrum.dart';
import 'package:cake_wallet/src/domain/common/transaction_info.dart';
import 'package:cake_wallet/src/domain/common/transaction_direction.dart';
part 'bitcoin_transaction_history.g.dart'; part 'bitcoin_transaction_history.g.dart';
@ -24,100 +22,176 @@ abstract class BitcoinTransactionHistoryBase
{this.eclient, String dirPath, @required String password}) {this.eclient, String dirPath, @required String password})
: path = '$dirPath/$_transactionsHistoryFileName', : path = '$dirPath/$_transactionsHistoryFileName',
_password = password, _password = password,
_height = 0; _height = 0,
_isUpdating = false {
transactions = ObservableMap<String, BitcoinTransactionInfo>();
}
BitcoinWalletBase wallet; BitcoinWalletBase wallet;
final ElectrumClient eclient; final ElectrumClient eclient;
final String path; final String path;
final String _password; final String _password;
int _height; int _height;
bool _isUpdating;
Future<void> init() async { Future<void> init() async {
final info = await _read(); await _load();
_height = info['height'] as int ?? _height;
transactions = ObservableList.of(
info['transactions'] as List<BitcoinTransactionInfo> ??
<BitcoinTransactionInfo>[]);
} }
@override @override
Future update() async { Future update() async {
await super.update(); if (_isUpdating) {
_updateHeight(); return;
}
try {
_isUpdating = true;
final txs = await fetchTransactions();
await add(txs);
_isUpdating = false;
} catch (_) {
_isUpdating = false;
rethrow;
}
} }
@override @override
Future<List<BitcoinTransactionInfo>> fetchTransactions() async { Future<Map<String, BitcoinTransactionInfo>> fetchTransactions() async {
final addresses = wallet.addresses;
final histories = final histories =
addresses.map((record) => eclient.getHistory(address: record.address)); wallet.scriptHashes.map((scriptHash) => eclient.getHistory(scriptHash));
final _historiesWithDetails = await Future.wait(histories) final _historiesWithDetails = await Future.wait(histories)
.then((histories) => histories .then((histories) => histories
.map((h) => h.where((tx) => (tx['height'] as int) > _height)) // .map((h) => h.where((tx) {
// final height = tx['height'] as int ?? 0;
// // FIXME: Filter only needed transactions
// final _tx = get(tx['tx_hash'] as String);
//
// return height == 0 || height > _height;
// }))
.expand((i) => i) .expand((i) => i)
.toList()) .toList())
.then((histories) => histories.map((tx) => fetchTransactionInfo( .then((histories) => histories.map((tx) => fetchTransactionInfo(
hash: tx['tx_hash'] as String, height: tx['height'] as int))); hash: tx['tx_hash'] as String, height: tx['height'] as int)));
final historiesWithDetails = await Future.wait(_historiesWithDetails); final historiesWithDetails = await Future.wait(_historiesWithDetails);
return historiesWithDetails return historiesWithDetails.fold<Map<String, BitcoinTransactionInfo>>(
.map((info) => BitcoinTransactionInfo.fromHexAndHeader( <String, BitcoinTransactionInfo>{}, (acc, tx) {
info['raw'] as String, info['header'] as Map<String, Object>, acc[tx.id] = tx;
addresses: addresses.map((record) => record.address).toList())) return acc;
.toList(); });
} }
Future<Map<String, Object>> fetchTransactionInfo( Future<BitcoinTransactionInfo> fetchTransactionInfo(
{@required String hash, @required int height}) async { {@required String hash, @required int height}) async {
final rawFetching = eclient.getTransactionRaw(hash: hash); final tx = await eclient.getTransactionExpanded(hash: hash);
final headerFetching = eclient.getHeader(height: height); return BitcoinTransactionInfo.fromElectrumVerbose(tx,
final result = await Future.wait([rawFetching, headerFetching]); height: height, addresses: wallet.addresses);
final raw = result.first as String;
final header = result[1] as Map<String, Object>;
return {'raw': raw, 'header': header};
} }
Future<void> add(List<BitcoinTransactionInfo> transactions) async { Future<void> add(Map<String, BitcoinTransactionInfo> transactionsList) async {
this.transactions.addAll(transactions); transactionsList.entries.forEach((entry) {
_updateOrInsert(entry.value);
if (entry.value.height > _height) {
_height = entry.value.height;
}
});
await save(); await save();
} }
Future<void> addOne(BitcoinTransactionInfo tx) async { Future<void> addOne(BitcoinTransactionInfo tx) async {
transactions.add(tx); _updateOrInsert(tx);
if (tx.height > _height) {
_height = tx.height;
}
await save(); await save();
} }
Future<void> save() async => writeData( BitcoinTransactionInfo get(String id) => transactions[id];
path: path,
password: _password, Future<void> save() async {
data: json.encode({'height': _height, 'transactions': transactions})); final data = json.encode({'height': _height, 'transactions': transactions});
print('data');
print(data);
await writeData(path: path, password: _password, data: data);
}
@override
void updateAsync({void Function() onFinished}) {
fetchTransactionsAsync((transaction) => _updateOrInsert(transaction),
onFinished: onFinished);
}
@override
void fetchTransactionsAsync(
void Function(BitcoinTransactionInfo transaction) onTransactionLoaded,
{void Function() onFinished}) async {
final histories = await Future.wait(wallet.scriptHashes
.map((scriptHash) async => await eclient.getHistory(scriptHash)));
final transactionsCount =
histories.fold<int>(0, (acc, m) => acc + m.length);
var counter = 0;
final batches = histories.map((metaList) =>
_fetchBatchOfTransactions(metaList, onTransactionLoaded: (transaction) {
onTransactionLoaded(transaction);
counter += 1;
if (counter == transactionsCount) {
onFinished?.call();
}
}));
await Future.wait(batches);
}
Future<void> _fetchBatchOfTransactions(
Iterable<Map<String, dynamic>> metaList,
{void Function(BitcoinTransactionInfo tranasaction)
onTransactionLoaded}) async =>
metaList.forEach((txMeta) => fetchTransactionInfo(
hash: txMeta['tx_hash'] as String,
height: txMeta['height'] as int)
.then((transaction) => onTransactionLoaded(transaction)));
Future<Map<String, Object>> _read() async { Future<Map<String, Object>> _read() async {
final content = await read(path: path, password: _password);
return json.decode(content) as Map<String, Object>;
}
Future<void> _load() async {
try { try {
final content = await read(path: path, password: _password); final content = await _read();
final jsoned = json.decode(content) as Map<String, Object>; final txs = content['transactions'] as Map<String, Object> ?? {};
final height = jsoned['height'] as int;
final transactions = (jsoned['transactions'] as List<dynamic>)
.map((dynamic row) {
if (row is Map<String, Object>) {
return BitcoinTransactionInfo.fromJson(row);
}
return null; txs.entries.forEach((entry) {
}) final val = entry.value;
.where((el) => el != null)
.toList();
return {'transactions': transactions, 'height': height}; if (val is Map<String, Object>) {
} catch (_) { final tx = BitcoinTransactionInfo.fromJson(val);
return {'transactions': <BitcoinTransactionInfo>[], 'height': 0}; _updateOrInsert(tx);
}
});
_height = content['height'] as int;
} catch (_) {}
}
void _updateOrInsert(BitcoinTransactionInfo transaction) {
if (transactions[transaction.id] == null) {
transactions[transaction.id] = transaction;
} else {
final originalTx = transactions[transaction.id];
originalTx.confirmations = transaction.confirmations;
originalTx.amount = transaction.amount;
originalTx.height = transaction.height;
originalTx.date ??= transaction.date;
originalTx.isPending = transaction.isPending;
} }
} }
void _updateHeight() {
final newHeight = transactions.fold(
0, (int acc, val) => val.height > acc ? val.height : acc);
_height = newHeight > _height ? newHeight : _height;
}
} }

View file

@ -1,7 +1,9 @@
import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart';
import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData;
import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
import 'package:cake_wallet/src/domain/bitcoin/bitcoin_amount_format.dart';
import 'package:cake_wallet/src/domain/common/transaction_direction.dart'; import 'package:cake_wallet/src/domain/common/transaction_direction.dart';
import 'package:cake_wallet/src/domain/common/transaction_info.dart'; import 'package:cake_wallet/src/domain/common/transaction_info.dart';
import 'package:cake_wallet/src/domain/common/format_amount.dart'; import 'package:cake_wallet/src/domain/common/format_amount.dart';
@ -13,7 +15,8 @@ class BitcoinTransactionInfo extends TransactionInfo {
@required int amount, @required int amount,
@required TransactionDirection direction, @required TransactionDirection direction,
@required bool isPending, @required bool isPending,
@required DateTime date}) { @required DateTime date,
@required this.confirmations}) {
this.height = height; this.height = height;
this.amount = amount; this.amount = amount;
this.direction = direction; this.direction = direction;
@ -21,34 +24,87 @@ class BitcoinTransactionInfo extends TransactionInfo {
this.isPending = isPending; this.isPending = isPending;
} }
factory BitcoinTransactionInfo.fromHexAndHeader( factory BitcoinTransactionInfo.fromElectrumVerbose(Map<String, Object> obj,
String hex, Map<String, Object> header, {@required List<BitcoinAddressRecord> addresses, @required int height}) {
{List<String> addresses}) { final addressesSet = addresses.map((addr) => addr.address).toSet();
final id = obj['txid'] as String;
final vins = obj['vin'] as List<Object> ?? [];
final vout = (obj['vout'] as List<Object> ?? []);
final date = obj['time'] is int
? DateTime.fromMillisecondsSinceEpoch((obj['time'] as int) * 1000)
: DateTime.now();
final confirmations = obj['confirmations'] as int ?? 0;
var direction = TransactionDirection.incoming;
for (dynamic vin in vins) {
final vout = vin['vout'] as int;
final out = vin['tx']['vout'][vout] as Map;
final outAddresses =
(out['scriptPubKey']['addresses'] as List<Object>)?.toSet();
if (outAddresses?.intersection(addressesSet)?.isNotEmpty ?? false) {
direction = TransactionDirection.outgoing;
break;
}
}
final amount = vout.fold(0, (int acc, dynamic out) {
final outAddresses =
out['scriptPubKey']['addresses'] as List<Object> ?? [];
final ntrs = outAddresses.toSet().intersection(addressesSet);
var amount = acc;
if ((direction == TransactionDirection.incoming && ntrs.isNotEmpty) ||
(direction == TransactionDirection.outgoing && ntrs.isEmpty)) {
amount += doubleToBitcoinAmount(out['value'] as double ?? 0.0);
}
return amount;
});
return BitcoinTransactionInfo(
id: id,
height: height,
isPending: false,
direction: direction,
amount: amount,
date: date,
confirmations: confirmations);
}
factory BitcoinTransactionInfo.fromHexAndHeader(String hex,
{List<String> addresses, int height, int timestamp, int confirmations}) {
final tx = bitcoin.Transaction.fromHex(hex); final tx = bitcoin.Transaction.fromHex(hex);
var exist = false; var exist = false;
var amount = 0; var amount = 0;
tx.outs.forEach((out) { if (addresses != null) {
try { tx.outs.forEach((out) {
final p2pkh = bitcoin.P2PKH( try {
data: PaymentData(output: out.script), network: bitcoin.bitcoin); final p2pkh = bitcoin.P2PKH(
exist = addresses.contains(p2pkh.data.address); data: PaymentData(output: out.script), network: bitcoin.bitcoin);
exist = addresses.contains(p2pkh.data.address);
if (exist) { if (exist) {
amount += out.value; amount += out.value;
} }
} catch (_) {} } catch (_) {}
}); });
}
final date = timestamp != null
? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000)
: DateTime.now();
// FIXME: Get transaction is pending // FIXME: Get transaction is pending
return BitcoinTransactionInfo( return BitcoinTransactionInfo(
id: tx.getId(), id: tx.getId(),
height: header['block_height'] as int, height: height,
isPending: false, isPending: false,
direction: TransactionDirection.incoming, direction: TransactionDirection.incoming,
amount: amount, amount: amount,
date: DateTime.fromMillisecondsSinceEpoch( date: date,
(header['timestamp'] as int) * 1000)); confirmations: confirmations);
} }
factory BitcoinTransactionInfo.fromJson(Map<String, dynamic> data) { factory BitcoinTransactionInfo.fromJson(Map<String, dynamic> data) {
@ -58,15 +114,18 @@ class BitcoinTransactionInfo extends TransactionInfo {
amount: data['amount'] as int, amount: data['amount'] as int,
direction: parseTransactionDirectionFromInt(data['direction'] as int), direction: parseTransactionDirectionFromInt(data['direction'] as int),
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
isPending: data['isPending'] as bool); isPending: data['isPending'] as bool,
confirmations: data['confirmations'] as int);
} }
final String id; final String id;
int confirmations;
String _fiatAmount; String _fiatAmount;
@override @override
String amountFormatted() => '${formatAmount(bitcoinAmountToString(amount: amount))} BTC'; String amountFormatted() =>
'${formatAmount(bitcoinAmountToString(amount: amount))} BTC';
@override @override
String fiatAmount() => _fiatAmount ?? ''; String fiatAmount() => _fiatAmount ?? '';
@ -75,13 +134,14 @@ class BitcoinTransactionInfo extends TransactionInfo {
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final m = Map<String, dynamic>(); final m = <String, dynamic>{};
m['id'] = id; m['id'] = id;
m['height'] = height; m['height'] = height;
m['amount'] = amount; m['amount'] = amount;
m['direction'] = direction.index; m['direction'] = direction.index;
m['date'] = date.millisecondsSinceEpoch; m['date'] = date.millisecondsSinceEpoch;
m['isPending'] = isPending; m['isPending'] = isPending;
m['confirmations'] = confirmations;
return m; return m;
} }
} }

View file

@ -0,0 +1,4 @@
class BitcoinTransactionNoInputsException implements Exception {
@override
String toString() => 'No inputs for the transaction.';
}

View file

@ -0,0 +1,4 @@
class BitcoinTransactionWrongBalanceException implements Exception {
@override
String toString() => 'Wrong balance. Not enough BTC on your balance.';
}

View file

@ -0,0 +1,17 @@
import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart';
class BitcoinUnspent {
BitcoinUnspent(this.address, this.hash, this.value, this.vout);
factory BitcoinUnspent.fromJSON(
BitcoinAddressRecord address, Map<String, dynamic> json) =>
BitcoinUnspent(address, json['tx_hash'] as String, json['value'] as int,
json['tx_pos'] as int);
final BitcoinAddressRecord address;
final String hash;
final int value;
final int vout;
bool get isP2wpkh => address.address.startsWith('bc1');
}

View file

@ -1,10 +1,20 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:convert'; import 'dart:convert';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_no_inputs_exception.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_wrong_balance_exception.dart';
import 'package:cake_wallet/bitcoin/bitcoin_unspent.dart';
import 'package:cake_wallet/bitcoin/bitcoin_wallet_keys.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet_keys.dart';
import 'package:cake_wallet/bitcoin/pending_bitcoin_transaction.dart';
import 'package:cake_wallet/bitcoin/script_hash.dart';
import 'package:cake_wallet/bitcoin/utils.dart';
import 'package:cake_wallet/src/domain/bitcoin/bitcoin_amount_format.dart'; import 'package:cake_wallet/src/domain/bitcoin/bitcoin_amount_format.dart';
import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
import 'package:cake_wallet/src/domain/common/sync_status.dart'; import 'package:cake_wallet/src/domain/common/sync_status.dart';
import 'package:cake_wallet/src/domain/common/transaction_direction.dart';
import 'package:cake_wallet/src/domain/common/transaction_priority.dart';
import 'package:cw_monero/transaction_history.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:bip39/bip39.dart' as bip39; import 'package:bip39/bip39.dart' as bip39;
@ -20,12 +30,39 @@ import 'package:cake_wallet/bitcoin/bitcoin_balance.dart';
import 'package:cake_wallet/src/domain/common/node.dart'; import 'package:cake_wallet/src/domain/common/node.dart';
import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/core/wallet_base.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
import 'package:hex/hex.dart';
part 'bitcoin_wallet.g.dart'; part 'bitcoin_wallet.g.dart';
class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet;
abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store { abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
BitcoinWalletBase._internal(
{@required this.eclient,
@required this.path,
@required String password,
@required this.name,
List<BitcoinAddressRecord> initialAddresses,
int accountIndex = 0,
this.transactionHistory,
this.mnemonic,
BitcoinBalance initialBalance})
: balance =
initialBalance ?? BitcoinBalance(confirmed: 0, unconfirmed: 0),
hd = bitcoin.HDWallet.fromSeed(bip39.mnemonicToSeed(mnemonic),
network: bitcoin.bitcoin),
addresses = initialAddresses != null
? ObservableList<BitcoinAddressRecord>.of(initialAddresses)
: ObservableList<BitcoinAddressRecord>(),
syncStatus = NotConnectedSyncStatus(),
_password = password,
_accountIndex = accountIndex,
_addressesKeys = {} {
type = WalletType.bitcoin;
currency = CryptoCurrency.btc;
_scripthashesUpdateSubject = {};
}
static BitcoinWallet fromJSON( static BitcoinWallet fromJSON(
{@required String password, {@required String password,
@required String name, @required String name,
@ -37,12 +74,12 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
(data['account_index'] == 'null' || data['account_index'] == null) (data['account_index'] == 'null' || data['account_index'] == null)
? 0 ? 0
: int.parse(data['account_index'] as String); : int.parse(data['account_index'] as String);
final _addresses = data['addresses'] as List; final _addresses = data['addresses'] as List ?? <Object>[];
final addresses = <BitcoinAddressRecord>[]; final addresses = <BitcoinAddressRecord>[];
final balance = BitcoinBalance.fromJSON(data['balance'] as String) ?? final balance = BitcoinBalance.fromJSON(data['balance'] as String) ??
BitcoinBalance(confirmed: 0, unconfirmed: 0); BitcoinBalance(confirmed: 0, unconfirmed: 0);
_addresses?.forEach((Object el) { _addresses.forEach((Object el) {
if (el is String) { if (el is String) {
addresses.add(BitcoinAddressRecord.fromJSON(el)); addresses.add(BitcoinAddressRecord.fromJSON(el));
} }
@ -83,34 +120,10 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
transactionHistory: history); transactionHistory: history);
} }
BitcoinWalletBase._internal(
{@required this.eclient,
@required this.path,
@required String password,
@required this.name,
List<BitcoinAddressRecord> initialAddresses,
int accountIndex = 0,
this.transactionHistory,
this.mnemonic,
BitcoinBalance initialBalance}) {
type = WalletType.bitcoin;
currency = CryptoCurrency.btc;
balance = initialBalance ?? BitcoinBalance(confirmed: 0, unconfirmed: 0);
hd = bitcoin.HDWallet.fromSeed(bip39.mnemonicToSeed(mnemonic),
network: bitcoin.bitcoin);
addresses = initialAddresses != null
? ObservableList<BitcoinAddressRecord>.of(initialAddresses)
: ObservableList<BitcoinAddressRecord>();
syncStatus = NotConnectedSyncStatus();
_password = password;
_accountIndex = accountIndex;
}
@override @override
final BitcoinTransactionHistory transactionHistory; final BitcoinTransactionHistory transactionHistory;
final String path; final String path;
bitcoin.HDWallet hd; final bitcoin.HDWallet hd;
final ElectrumClient eclient; final ElectrumClient eclient;
final String mnemonic; final String mnemonic;
@ -131,6 +144,11 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
ObservableList<BitcoinAddressRecord> addresses; ObservableList<BitcoinAddressRecord> addresses;
Map<String, bitcoin.ECPair> _addressesKeys;
List<String> get scriptHashes =>
addresses.map((addr) => scriptHash(addr.address)).toList();
String get xpub => hd.base58; String get xpub => hd.base58;
@override @override
@ -142,11 +160,13 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
int _accountIndex; int _accountIndex;
String _password; String _password;
BehaviorSubject<Object> _addressUpdateSubject; Map<String, BehaviorSubject<Object>> _scripthashesUpdateSubject;
Future<void> init() async { Future<void> init() async {
if (addresses.isEmpty) { if (addresses.isEmpty) {
addresses.add(BitcoinAddressRecord(_getAddress(hd: hd, index: 0))); final index = 0;
addresses
.add(BitcoinAddressRecord(_getAddress(index: index), index: index));
} }
address = addresses.first.address; address = addresses.first.address;
@ -156,9 +176,8 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
Future<BitcoinAddressRecord> generateNewAddress({String label}) async { Future<BitcoinAddressRecord> generateNewAddress({String label}) async {
_accountIndex += 1; _accountIndex += 1;
final address = BitcoinAddressRecord( final address = BitcoinAddressRecord(_getAddress(index: _accountIndex),
_getAddress(hd: hd, index: _accountIndex), index: _accountIndex, label: label);
label: label);
addresses.add(address); addresses.add(address);
await save(); await save();
@ -181,9 +200,8 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
Future<void> startSync() async { Future<void> startSync() async {
try { try {
syncStatus = StartingSyncStatus(); syncStatus = StartingSyncStatus();
await _addressUpdateSubject?.close(); transactionHistory.updateAsync(onFinished: () => print('finished!'));
_addressUpdateSubject = eclient.addressUpdate(address: address); _subscribeForUpdates();
await transactionHistory.update();
await _updateBalance(); await _updateBalance();
syncStatus = SyncedSyncStatus(); syncStatus = SyncedSyncStatus();
} catch (e) { } catch (e) {
@ -197,39 +215,102 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
Future<void> connectToNode({@required Node node}) async { Future<void> connectToNode({@required Node node}) async {
try { try {
syncStatus = ConnectingSyncStatus(); syncStatus = ConnectingSyncStatus();
await eclient.connect(host: 'electrum2.hodlister.co', port: 50002); // electrum2.hodlister.co
// bitcoin.electrumx.multicoin.co:50002
// electrum2.taborsky.cz:5002
await eclient.connect(
host: 'bitcoin.electrumx.multicoin.co', port: 50002);
syncStatus = ConnectedSyncStatus(); syncStatus = ConnectedSyncStatus();
} catch (e) { } catch (e) {
print(e.toString); print(e.toString());
syncStatus = FailedSyncStatus(); syncStatus = FailedSyncStatus();
} }
} }
@override @override
Future<void> createTransaction(Object credentials) async { Future<PendingBitcoinTransaction> createTransaction(
Object credentials) async {
final transactionCredentials = credentials as BitcoinTransactionCredentials; final transactionCredentials = credentials as BitcoinTransactionCredentials;
final inputs = <BitcoinUnspent>[];
final fee = _feeMultiplier(transactionCredentials.priority);
final amount = transactionCredentials.amount != null
? doubleToBitcoinAmount(transactionCredentials.amount)
: balance.total - fee;
final totalAmount = amount + fee;
final txb = bitcoin.TransactionBuilder(network: bitcoin.bitcoin); final txb = bitcoin.TransactionBuilder(network: bitcoin.bitcoin);
final keyPair = bitcoin.ECPair.fromWIF(hd.wif); var leftAmount = totalAmount;
final transactions = transactionHistory.transactions; final changeAddress = address;
transactions.sort((q, w) => q.height.compareTo(w.height)); var totalInputAmount = 0;
final prevTx = transactions.first;
final unspent = addresses.map((address) => eclient
.getListUnspentWithAddress(address.address)
.then((unspent) => unspent
.map((unspent) => BitcoinUnspent.fromJSON(address, unspent))));
for (final unptsFutures in unspent) {
final utxs = await unptsFutures;
for (final utx in utxs) {
final inAmount = utx.value > totalAmount ? totalAmount : utx.value;
leftAmount = leftAmount - inAmount;
totalInputAmount += inAmount;
inputs.add(utx);
if (leftAmount <= 0) {
break;
}
}
if (leftAmount <= 0) {
break;
}
}
if (inputs.isEmpty) {
throw BitcoinTransactionNoInputsException();
}
if (amount <= 0 || totalInputAmount < amount) {
throw BitcoinTransactionWrongBalanceException();
}
final changeValue = totalInputAmount - amount - fee;
txb.setVersion(1); txb.setVersion(1);
txb.addInput(prevTx, 0);
txb.addOutput(transactionCredentials.address,
doubleToBitcoinAmount(transactionCredentials.amount));
txb.sign(vin: 0, keyPair: keyPair);
final encoded = txb.build().toHex();
print('Enoded transaction $encoded'); inputs.forEach((input) {
await eclient.broadcastTransaction(transactionRaw: encoded); if (input.isP2wpkh) {
final p2wpkh = bitcoin
.P2WPKH(
data: generatePaymentData(hd: hd, index: input.address.index),
network: bitcoin.bitcoin)
.data;
txb.addInput(input.hash, input.vout, null, p2wpkh.output);
} else {
txb.addInput(input.hash, input.vout);
}
});
txb.addOutput(transactionCredentials.address, amount);
if (changeValue > 0) {
txb.addOutput(changeAddress, changeValue);
}
for (var i = 0; i < inputs.length; i++) {
final input = inputs[i];
final keyPair = generateKeyPair(hd: hd, index: input.address.index);
final witnessValue = input.isP2wpkh ? input.value : null;
txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue);
}
return PendingBitcoinTransaction(txb.build(),
eclient: eclient, amount: amount, fee: fee)
..addListener((transaction) => transactionHistory.addOne(transaction));
} }
@override
Future<void> save() async =>
await write(path: path, password: _password, data: toJSON());
String toJSON() => json.encode({ String toJSON() => json.encode({
'mnemonic': mnemonic, 'mnemonic': mnemonic,
'account_index': _accountIndex.toString(), 'account_index': _accountIndex.toString(),
@ -237,16 +318,32 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
'balance': balance?.toJSON() 'balance': balance?.toJSON()
}); });
String _getAddress({bitcoin.HDWallet hd, int index}) => bitcoin @override
.P2WPKH( double calculateEstimatedFee(TransactionPriority priority) =>
data: PaymentData( bitcoinAmountToDouble(amount: _feeMultiplier(priority));
pubkey: Uint8List.fromList(hd.derive(index).pubKey.codeUnits)))
.data @override
.address; Future<void> save() async =>
await write(path: path, password: _password, data: toJSON());
bitcoin.ECPair keyPairFor({@required int index}) =>
generateKeyPair(hd: hd, index: index);
void _subscribeForUpdates() {
scriptHashes.forEach((sh) async {
await _scripthashesUpdateSubject[sh]?.close();
_scripthashesUpdateSubject[sh] = eclient.scripthashUpdate(sh);
_scripthashesUpdateSubject[sh].listen((event) async {
print('event $event');
transactionHistory.updateAsync();
await _updateBalance();
});
});
}
Future<BitcoinBalance> _fetchBalances() async { Future<BitcoinBalance> _fetchBalances() async {
final balances = await Future.wait( final balances = await Future.wait(
addresses.map((record) => eclient.getBalance(address: record.address))); scriptHashes.map((sHash) => eclient.getBalance(sHash)));
final balance = balances.fold( final balance = balances.fold(
BitcoinBalance(confirmed: 0, unconfirmed: 0), BitcoinBalance(confirmed: 0, unconfirmed: 0),
(BitcoinBalance acc, val) => BitcoinBalance( (BitcoinBalance acc, val) => BitcoinBalance(
@ -261,4 +358,20 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
balance = await _fetchBalances(); balance = await _fetchBalances();
await save(); await save();
} }
String _getAddress({@required int index}) =>
generateAddress(hd: hd, index: index);
int _feeMultiplier(TransactionPriority priority) {
switch (priority) {
case TransactionPriority.slow:
return 6000;
case TransactionPriority.regular:
return 9000;
case TransactionPriority.fast:
return 15000;
default:
return 0;
}
}
} }

View file

@ -1,17 +1,18 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:cake_wallet/bitcoin/script_hash.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
String jsonrpcparams(List<Object> params) { String jsonrpcparams(List<Object> params) {
final _params = params?.map((val) => '"${val.toString()}"')?.join(','); final _params = params?.map((val) => '"${val.toString()}"')?.join(',');
return "[$_params]"; return '[$_params]';
} }
String jsonrpc( String jsonrpc(
{String method, List<Object> params, int id, double version = 2.0}) => {String method, List<Object> params, int id, double version = 2.0}) =>
'{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${jsonrpcparams(params)}}\n'; '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json.encode(params)}}\n';
class SocketTask { class SocketTask {
SocketTask({this.completer, this.isSubscription, this.subject}); SocketTask({this.completer, this.isSubscription, this.subject});
@ -50,6 +51,7 @@ class ElectrumClient {
socket.listen((List<int> event) { socket.listen((List<int> event) {
try { try {
final jsoned = json.decode(utf8.decode(event)) as Map<String, Object>; final jsoned = json.decode(utf8.decode(event)) as Map<String, Object>;
// print(jsoned);
final method = jsoned['method']; final method = jsoned['method'];
if (method is String) { if (method is String) {
@ -93,18 +95,18 @@ class ElectrumClient {
return []; return [];
}); });
Future<Map<String, Object>> getBalance({String address}) => Future<Map<String, Object>> getBalance(String scriptHash) =>
call(method: 'blockchain.address.get_balance', params: [address]) call(method: 'blockchain.scripthash.get_balance', params: [scriptHash])
.then((dynamic result) { .then((dynamic result) {
if (result is Map<String, Object>) { if (result is Map<String, Object>) {
return result; return result;
} }
return Map<String, Object>(); return <String, Object>{};
}); });
Future<List<Map<String, dynamic>>> getHistory({String address}) => Future<List<Map<String, dynamic>>> getHistory(String scriptHash) =>
call(method: 'blockchain.address.get_history', params: [address]) call(method: 'blockchain.scripthash.get_history', params: [scriptHash])
.then((dynamic result) { .then((dynamic result) {
if (result is List) { if (result is List) {
return result.map((dynamic val) { return result.map((dynamic val) {
@ -112,26 +114,94 @@ class ElectrumClient {
return val; return val;
} }
return Map<String, Object>(); return <String, Object>{};
}).toList(); }).toList();
} }
return []; return [];
}); });
Future<String> getTransactionRaw({@required String hash}) async => Future<List<Map<String, dynamic>>> getListUnspentWithAddress(
call(method: 'blockchain.transaction.get', params: [hash]) String address) =>
call(
method: 'blockchain.scripthash.listunspent',
params: [scriptHash(address)]).then((dynamic result) {
if (result is List) {
return result.map((dynamic val) {
if (val is Map<String, Object>) {
val['address'] = address;
return val;
}
return <String, Object>{};
}).toList();
}
return [];
});
Future<List<Map<String, dynamic>>> getListUnspent(String scriptHash) =>
call(method: 'blockchain.scripthash.listunspent', params: [scriptHash])
.then((dynamic result) { .then((dynamic result) {
if (result is String) { if (result is List) {
return result.map((dynamic val) {
if (val is Map<String, Object>) {
return val;
}
return <String, Object>{};
}).toList();
}
return [];
});
Future<List<Map<String, dynamic>>> getMempool(String scriptHash) =>
call(method: 'blockchain.scripthash.get_mempool', params: [scriptHash])
.then((dynamic result) {
if (result is List) {
return result.map((dynamic val) {
if (val is Map<String, Object>) {
return val;
}
return <String, Object>{};
}).toList();
}
return [];
});
Future<Map<String, Object>> getTransactionRaw(
{@required String hash}) async =>
call(method: 'blockchain.transaction.get', params: [hash, true])
.then((dynamic result) {
if (result is Map<String, Object>) {
return result; return result;
} }
return ''; return <String, Object>{};
}); });
Future<String> broadcastTransaction({@required String transactionRaw}) async => Future<Map<String, Object>> getTransactionExpanded(
{@required String hash}) async {
final originalTx = await getTransactionRaw(hash: hash);
final vins = originalTx['vin'] as List<Object>;
for (dynamic vin in vins) {
if (vin is Map<String, Object>) {
vin['tx'] = await getTransactionRaw(hash: vin['txid'] as String);
}
}
return originalTx;
}
Future<String> broadcastTransaction(
{@required String transactionRaw}) async =>
call(method: 'blockchain.transaction.broadcast', params: [transactionRaw]) call(method: 'blockchain.transaction.broadcast', params: [transactionRaw])
.then((dynamic result) { .then((dynamic result) {
print('result $result');
if (result is String) { if (result is String) {
return result; return result;
} }
@ -163,11 +233,11 @@ class ElectrumClient {
return 0; return 0;
}); });
BehaviorSubject<Object> addressUpdate({@required String address}) => BehaviorSubject<Object> scripthashUpdate(String scripthash) =>
subscribe<Object>( subscribe<Object>(
id: 'blockchain.address.subscribe:$address', id: 'blockchain.scripthash.subscribe:$scripthash',
method: 'blockchain.address.subscribe', method: 'blockchain.scripthash.subscribe',
params: [address]); params: [scripthash]);
BehaviorSubject<T> subscribe<T>( BehaviorSubject<T> subscribe<T>(
{@required String id, {@required String id,
@ -218,15 +288,12 @@ class ElectrumClient {
void _methodHandler( void _methodHandler(
{@required String method, @required Map<String, Object> request}) { {@required String method, @required Map<String, Object> request}) {
switch (method) { switch (method) {
case 'blockchain.address.subscribe': case 'blockchain.scripthash.subscribe':
final params = request['params'] as List<dynamic>; final params = request['params'] as List<dynamic>;
final address = params.first as String; final scripthash = params.first as String;
final id = 'blockchain.address.subscribe:$address'; final id = 'blockchain.scripthash.subscribe:$scripthash';
if (_tasks[id] != null) {
_tasks[id].subject.add(params.last);
}
_tasks[id]?.subject?.add(params.last);
break; break;
default: default:
break; break;

View file

@ -0,0 +1,47 @@
import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart';
import 'package:cake_wallet/src/domain/common/transaction_direction.dart';
import 'package:flutter/foundation.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:cake_wallet/core/pending_transaction.dart';
import 'package:cake_wallet/bitcoin/electrum.dart';
class PendingBitcoinTransaction with PendingTransaction {
PendingBitcoinTransaction(this._tx,
{@required this.eclient, @required this.amount, @required this.fee})
: _listeners = <void Function(BitcoinTransactionInfo transaction)>[];
final bitcoin.Transaction _tx;
final ElectrumClient eclient;
final int amount;
final int fee;
String get id => _tx.getId();
@override
String get amountFormatted => bitcoinAmountToString(amount: amount);
@override
String get feeFormatted => bitcoinAmountToString(amount: fee);
final List<void Function(BitcoinTransactionInfo transaction)> _listeners;
@override
Future<void> commit() async {
await eclient.broadcastTransaction(transactionRaw: _tx.toHex());
_listeners?.forEach((listener) => listener(transactionInfo()));
}
void addListener(
void Function(BitcoinTransactionInfo transaction) listener) =>
_listeners.add(listener);
BitcoinTransactionInfo transactionInfo() => BitcoinTransactionInfo(
id: id,
height: 0,
amount: amount,
direction: TransactionDirection.outgoing,
date: DateTime.now(),
isPending: true,
confirmations: 0);
}

View file

@ -0,0 +1,18 @@
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:crypto/crypto.dart';
String scriptHash(String address) {
final outputScript = bitcoin.Address.addressToOutputScript(address);
final splitted = sha256.convert(outputScript).toString().split('');
var res = '';
for (var i = splitted.length - 1; i >= 0; i--) {
final char = splitted[i];
i--;
final nextChar = splitted[i];
res += nextChar;
res += char;
}
return res;
}

26
lib/bitcoin/utils.dart Normal file
View file

@ -0,0 +1,26 @@
import 'dart:typed_data';
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:hex/hex.dart';
bitcoin.PaymentData generatePaymentData(
{@required bitcoin.HDWallet hd, @required int index}) =>
PaymentData(
pubkey: Uint8List.fromList(HEX.decode(hd.derive(index).pubKey)));
bitcoin.ECPair generateKeyPair(
{@required bitcoin.HDWallet hd,
@required int index,
bitcoin.NetworkType network}) =>
bitcoin.ECPair.fromWIF(hd.derive(index).wif,
network: network ?? bitcoin.bitcoin);
String generateAddress({@required bitcoin.HDWallet hd, @required int index}) =>
bitcoin
.P2WPKH(
data: PaymentData(
pubkey:
Uint8List.fromList(HEX.decode(hd.derive(index).pubKey))))
.data
.address;

View file

@ -13,10 +13,10 @@ class AmountValidator extends TextValidator {
static String _pattern(WalletType type) { static String _pattern(WalletType type) {
switch (type) { switch (type) {
case WalletType.monero: case WalletType.monero:
return '^([0-9]+([.][0-9]{0,12})?|[.][0-9]{1,12})\$'; return '^([0-9]+([.\,][0-9]{0,12})?|[.\,][0-9]{1,12})\$';
case WalletType.bitcoin: case WalletType.bitcoin:
// FIXME: Incorrect pattern for bitcoin // FIXME: Incorrect pattern for bitcoin
return '^([0-9]+([.][0-9]{0,12})?|[.][0-9]{1,12})\$'; return '^([0-9]+([.\,][0-9]{0,12})?|[.\,][0-9]{1,12})\$';
default: default:
return ''; return '';
} }

View file

@ -0,0 +1,6 @@
mixin PendingTransaction {
String get amountFormatted;
String get feeFormatted;
Future<void> commit();
}

View file

@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cake_wallet/src/domain/common/transaction_info.dart'; import 'package:cake_wallet/src/domain/common/transaction_info.dart';
@ -5,7 +6,7 @@ abstract class TransactionHistoryBase<TransactionType extends TransactionInfo> {
TransactionHistoryBase() : _isUpdating = false; TransactionHistoryBase() : _isUpdating = false;
@observable @observable
ObservableList<TransactionType> transactions; ObservableMap<String, TransactionType> transactions;
bool _isUpdating; bool _isUpdating;
@ -24,5 +25,15 @@ abstract class TransactionHistoryBase<TransactionType extends TransactionInfo> {
} }
} }
Future<List<TransactionType>> fetchTransactions(); void updateAsync({void Function() onFinished}) {
} fetchTransactionsAsync(
(transaction) => transactions[transaction.id] = transaction,
onFinished: onFinished);
}
void fetchTransactionsAsync(
void Function(TransactionType transaction) onTransactionLoaded,
{void Function() onFinished});
Future<Map<String, TransactionType>> fetchTransactions();
}

View file

@ -1,5 +1,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:cake_wallet/core/pending_transaction.dart';
import 'package:cake_wallet/core/transaction_history.dart'; import 'package:cake_wallet/core/transaction_history.dart';
import 'package:cake_wallet/src/domain/common/transaction_priority.dart';
import 'package:cake_wallet/src/domain/common/crypto_currency.dart'; import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
import 'package:cake_wallet/src/domain/common/sync_status.dart'; import 'package:cake_wallet/src/domain/common/sync_status.dart';
import 'package:cake_wallet/src/domain/common/node.dart'; import 'package:cake_wallet/src/domain/common/node.dart';
@ -30,7 +32,9 @@ abstract class WalletBase<BalaceType> {
Future<void> startSync(); Future<void> startSync();
Future<void> createTransaction(Object credentials); Future<PendingTransaction> createTransaction(Object credentials);
double calculateEstimatedFee(TransactionPriority priority);
Future<void> save(); Future<void> save();
} }

View file

@ -36,7 +36,7 @@ import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
import 'package:cake_wallet/view_model/send_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart';
import 'package:cake_wallet/view_model/settings/settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/settings_view_model.dart';
import 'package:cake_wallet/view_model/wallet_keys_view_model.dart'; import 'package:cake_wallet/view_model/wallet_keys_view_model.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
@ -105,8 +105,7 @@ Future setup(
getIt.registerSingleton<ContactService>( getIt.registerSingleton<ContactService>(
ContactService(contactSource, getIt.get<AppStore>().contactListStore)); ContactService(contactSource, getIt.get<AppStore>().contactListStore));
getIt.registerSingleton<TradesStore>(TradesStore( getIt.registerSingleton<TradesStore>(TradesStore(
tradesSource: tradesSource, tradesSource: tradesSource, settingsStore: getIt.get<SettingsStore>()));
settingsStore: getIt.get<SettingsStore>()));
getIt.registerSingleton<TradeFilterStore>( getIt.registerSingleton<TradeFilterStore>(
TradeFilterStore(wallet: getIt.get<AppStore>().wallet)); TradeFilterStore(wallet: getIt.get<AppStore>().wallet));
getIt.registerSingleton<TransactionFilterStore>(TransactionFilterStore()); getIt.registerSingleton<TransactionFilterStore>(TransactionFilterStore());
@ -143,21 +142,18 @@ Future setup(
getIt.registerFactory<WalletAddressListViewModel>( getIt.registerFactory<WalletAddressListViewModel>(
() => WalletAddressListViewModel(wallet: getIt.get<AppStore>().wallet)); () => WalletAddressListViewModel(wallet: getIt.get<AppStore>().wallet));
getIt.registerFactory( getIt.registerFactory(() => BalanceViewModel(
() => BalanceViewModel( wallet: getIt.get<AppStore>().wallet,
wallet: getIt.get<AppStore>().wallet, settingsStore: getIt.get<SettingsStore>(),
settingsStore: getIt.get<SettingsStore>(), fiatConvertationStore: getIt.get<FiatConvertationStore>()));
fiatConvertationStore: getIt.get<FiatConvertationStore>()));
getIt.registerFactory( getIt.registerFactory(() => DashboardViewModel(
() => DashboardViewModel( balanceViewModel: getIt.get<BalanceViewModel>(),
balanceViewModel: getIt.get<BalanceViewModel>(), appStore: getIt.get<AppStore>(),
appStore: getIt.get<AppStore>(), tradesStore: getIt.get<TradesStore>(),
tradesStore: getIt.get<TradesStore>(), tradeFilterStore: getIt.get<TradeFilterStore>(),
tradeFilterStore: getIt.get<TradeFilterStore>(), transactionFilterStore: getIt.get<TransactionFilterStore>(),
transactionFilterStore: getIt.get<TransactionFilterStore>(), pageViewStore: getIt.get<PageViewStore>()));
pageViewStore: getIt.get<PageViewStore>()
));
getIt.registerFactory<AuthService>(() => AuthService( getIt.registerFactory<AuthService>(() => AuthService(
secureStorage: getIt.get<FlutterSecureStorage>(), secureStorage: getIt.get<FlutterSecureStorage>(),
@ -185,10 +181,9 @@ Future setup(
onAuthenticationFinished: onAuthFinished, onAuthenticationFinished: onAuthFinished,
closable: false)); closable: false));
getIt.registerFactory<DashboardPage>( getIt.registerFactory<DashboardPage>(() => DashboardPage(
() => DashboardPage( walletViewModel: getIt.get<DashboardViewModel>(),
walletViewModel: getIt.get<DashboardViewModel>(), addressListViewModel: getIt.get<WalletAddressListViewModel>()));
addressListViewModel: getIt.get<WalletAddressListViewModel>()));
getIt.registerFactory<ReceivePage>(() => ReceivePage( getIt.registerFactory<ReceivePage>(() => ReceivePage(
addressListViewModel: getIt.get<WalletAddressListViewModel>())); addressListViewModel: getIt.get<WalletAddressListViewModel>()));
@ -203,7 +198,9 @@ Future setup(
getIt.get<WalletAddressEditOrCreateViewModel>(param1: item))); getIt.get<WalletAddressEditOrCreateViewModel>(param1: item)));
getIt.registerFactory<SendViewModel>(() => SendViewModel( getIt.registerFactory<SendViewModel>(() => SendViewModel(
getIt.get<AppStore>().wallet, getIt.get<AppStore>().settingsStore)); getIt.get<AppStore>().wallet,
getIt.get<AppStore>().settingsStore,
getIt.get<FiatConvertationStore>()));
getIt.registerFactory( getIt.registerFactory(
() => SendPage(sendViewModel: getIt.get<SendViewModel>())); () => SendPage(sendViewModel: getIt.get<SendViewModel>()));
@ -243,8 +240,10 @@ Future setup(
moneroAccountCreationViewModel: moneroAccountCreationViewModel:
getIt.get<MoneroAccountEditOrCreateViewModel>())); getIt.get<MoneroAccountEditOrCreateViewModel>()));
getIt.registerFactory( getIt.registerFactory(() {
() => SettingsViewModel(getIt.get<AppStore>().settingsStore)); final appStore = getIt.get<AppStore>();
return SettingsViewModel(appStore.settingsStore, appStore.wallet);
});
getIt.registerFactory(() => SettingsPage(getIt.get<SettingsViewModel>())); getIt.registerFactory(() => SettingsPage(getIt.get<SettingsViewModel>()));

View file

@ -20,12 +20,29 @@ class MoneroTransactionHistory = MoneroTransactionHistoryBase
abstract class MoneroTransactionHistoryBase abstract class MoneroTransactionHistoryBase
extends TransactionHistoryBase<MoneroTransactionInfo> with Store { extends TransactionHistoryBase<MoneroTransactionInfo> with Store {
MoneroTransactionHistoryBase() { MoneroTransactionHistoryBase() {
transactions = ObservableList<MoneroTransactionInfo>(); transactions = ObservableMap<String, MoneroTransactionInfo>();
} }
@override @override
Future<List<MoneroTransactionInfo>> fetchTransactions() async { Future<Map<String, MoneroTransactionInfo>> fetchTransactions() async {
monero_transaction_history.refreshTransactions(); monero_transaction_history.refreshTransactions();
return _getAllTransactions(null); return _getAllTransactions(null).fold<Map<String, MoneroTransactionInfo>>(
<String, MoneroTransactionInfo>{},
(Map<String, MoneroTransactionInfo> acc, MoneroTransactionInfo tx) {
acc[tx.id] = tx;
return acc;
});
} }
@override
void updateAsync({void Function() onFinished}) {
fetchTransactionsAsync(
(transaction) => transactions[transaction.id] = transaction,
onFinished: onFinished);
}
@override
void fetchTransactionsAsync(
void Function(MoneroTransactionInfo transaction) onTransactionLoaded,
{void Function() onFinished}) {}
} }

View file

@ -16,6 +16,9 @@ import 'package:cake_wallet/src/domain/monero/account.dart';
import 'package:cake_wallet/src/domain/monero/account_list.dart'; import 'package:cake_wallet/src/domain/monero/account_list.dart';
import 'package:cake_wallet/src/domain/monero/subaddress.dart'; import 'package:cake_wallet/src/domain/monero/subaddress.dart';
import 'package:cake_wallet/src/domain/common/node.dart'; import 'package:cake_wallet/src/domain/common/node.dart';
import 'package:cake_wallet/core/pending_transaction.dart';
import 'package:cake_wallet/src/domain/common/transaction_priority.dart';
import 'package:cake_wallet/src/domain/common/calculate_fiat_amount.dart' as cfa;
part 'monero_wallet.g.dart'; part 'monero_wallet.g.dart';
@ -133,7 +136,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
} }
@override @override
Future<void> createTransaction(Object credentials) async { Future<PendingTransaction> createTransaction(Object credentials) async {
// final _credentials = credentials as MoneroTransactionCreationCredentials; // final _credentials = credentials as MoneroTransactionCreationCredentials;
// final transactionDescription = await transaction_history.createTransaction( // final transactionDescription = await transaction_history.createTransaction(
// address: _credentials.address, // address: _credentials.address,
@ -146,6 +149,33 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
// transactionDescription); // transactionDescription);
} }
@override
double calculateEstimatedFee(TransactionPriority priority) {
// FIXME: hardcoded value;
if (priority == TransactionPriority.slow) {
return 0.00002459;
}
if (priority == TransactionPriority.regular) {
return 0.00012305;
}
if (priority == TransactionPriority.medium) {
return 0.00024503;
}
if (priority == TransactionPriority.fast) {
return 0.00061453;
}
if (priority == TransactionPriority.fastest) {
return 0.0260216;
}
return 0;
}
@override @override
Future<void> save() async { Future<void> save() async {
// if (_isSaving) { // if (_isSaving) {

View file

@ -1,6 +1,7 @@
import 'package:cake_wallet/src/domain/common/transaction_direction.dart'; import 'package:cake_wallet/src/domain/common/transaction_direction.dart';
abstract class TransactionInfo extends Object { abstract class TransactionInfo extends Object {
String id;
int amount; int amount;
TransactionDirection direction; TransactionDirection direction;
bool isPending; bool isPending;

View file

@ -1,40 +1,27 @@
import 'package:cake_wallet/core/address_validator.dart'; import 'dart:ui';
import 'package:cake_wallet/core/amount_validator.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/view_model/send_view_model.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:provider/provider.dart';
import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:cake_wallet/src/widgets/address_text_field.dart'; import 'package:cake_wallet/src/widgets/address_text_field.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/stores/settings/settings_store.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/stores/balance/balance_store.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart';
import 'package:cake_wallet/src/stores/wallet/wallet_store.dart';
import 'package:cake_wallet/src/stores/send/send_store.dart';
//import 'package:cake_wallet/src/stores/send/sending_state.dart';
import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
import 'package:cake_wallet/src/domain/common/calculate_estimated_fee.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/domain/common/sync_status.dart';
import 'package:cake_wallet/src/stores/sync/sync_store.dart';
import 'package:cake_wallet/src/widgets/top_panel.dart'; import 'package:cake_wallet/src/widgets/top_panel.dart';
import 'package:dotted_border/dotted_border.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart';
import 'package:cake_wallet/src/screens/send/widgets/sending_alert.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
import 'package:cake_wallet/src/widgets/template_tile.dart';
import 'package:cake_wallet/src/stores/send_template/send_template_store.dart';
import 'package:cake_wallet/src/widgets/trail_button.dart'; import 'package:cake_wallet/src/widgets/trail_button.dart';
// FIXME: Refactor this screen.
class SendPage extends BasePage { class SendPage extends BasePage {
SendPage({@required this.sendViewModel}); SendPage({@required this.sendViewModel});
@ -53,11 +40,8 @@ class SendPage extends BasePage {
bool get resizeToAvoidBottomPadding => false; bool get resizeToAvoidBottomPadding => false;
@override @override
Widget trailing(context) { Widget trailing(context) => TrailButton(
// final sendStore = Provider.of<SendStore>(context); caption: S.of(context).clear, onPressed: () => sendViewModel.reset());
return TrailButton(caption: S.of(context).clear, onPressed: () => null);
}
@override @override
Widget body(BuildContext context) => SendForm(sendViewModel: sendViewModel); Widget body(BuildContext context) => SendForm(sendViewModel: sendViewModel);
@ -95,36 +79,28 @@ class SendFormState extends State<SendForm> {
} }
Future<void> getOpenaliasRecord(BuildContext context) async { Future<void> getOpenaliasRecord(BuildContext context) async {
final sendStore = Provider.of<SendStore>(context); // final sendStore = Provider.of<SendStore>(context);
final isOpenalias = // final isOpenalias =
await sendStore.isOpenaliasRecord(_addressController.text); // await sendStore.isOpenaliasRecord(_addressController.text);
//
if (isOpenalias) { // if (isOpenalias) {
_addressController.text = sendStore.recordAddress; // _addressController.text = sendStore.recordAddress;
//
await showDialog<void>( // await showDialog<void>(
context: context, // context: context,
builder: (BuildContext context) { // builder: (BuildContext context) {
return AlertWithOneAction( // return AlertWithOneAction(
alertTitle: S.of(context).openalias_alert_title, // alertTitle: S.of(context).openalias_alert_title,
alertContent: // alertContent:
S.of(context).openalias_alert_content(sendStore.recordName), // S.of(context).openalias_alert_content(sendStore.recordName),
buttonText: S.of(context).ok, // buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop()); // buttonAction: () => Navigator.of(context).pop());
}); // });
} // }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// final settingsStore = Provider.of<SettingsStore>(context);
// final sendStore = Provider.of<SendStore>(context);
// sendStore.settingsStore = settingsStore;
// final balanceStore = Provider.of<BalanceStore>(context);
// final walletStore = Provider.of<WalletStore>(context);
// final syncStore = Provider.of<SyncStore>(context);
// final sendTemplateStore = Provider.of<SendTemplateStore>(context);
_setEffects(context); _setEffects(context);
return Container( return Container(
@ -140,7 +116,8 @@ class SendFormState extends State<SendForm> {
child: Column(children: <Widget>[ child: Column(children: <Widget>[
AddressTextField( AddressTextField(
controller: _addressController, controller: _addressController,
placeholder: S.of(context).send_monero_address, placeholder: 'Address',
//S.of(context).send_monero_address, FIXME: placeholder for btc and xmr address text field.
focusNode: _focusNode, focusNode: _focusNode,
onURIScanned: (uri) { onURIScanned: (uri) {
var address = ''; var address = '';
@ -163,110 +140,86 @@ class SendFormState extends State<SendForm> {
buttonColor: Theme.of(context).accentTextTheme.title.color, buttonColor: Theme.of(context).accentTextTheme.title.color,
validator: widget.sendViewModel.addressValidator, validator: widget.sendViewModel.addressValidator,
), ),
Observer(builder: (_) { Padding(
return Padding( padding: const EdgeInsets.only(top: 20),
padding: const EdgeInsets.only(top: 20), child: TextFormField(
child: TextFormField( onChanged: (value) =>
style: TextStyle( widget.sendViewModel.setCryptoAmount(value),
fontSize: 16.0, style: TextStyle(
color: Theme.of(context) fontSize: 16.0,
.primaryTextTheme color:
.title Theme.of(context).primaryTextTheme.title.color),
.color), controller: _cryptoAmountController,
controller: _cryptoAmountController, keyboardType: TextInputType.numberWithOptions(
keyboardType: TextInputType.numberWithOptions( signed: false, decimal: true),
signed: false, decimal: true), // inputFormatters: [
inputFormatters: [ // BlacklistingTextInputFormatter(
BlacklistingTextInputFormatter( // RegExp('[\\-|\\ |\\,]'))
RegExp('[\\-|\\ |\\,]')) // ],
], decoration: InputDecoration(
decoration: InputDecoration( prefixIcon: Padding(
prefixIcon: Padding( padding: EdgeInsets.only(top: 12),
padding: EdgeInsets.only(top: 12), child: Text('${widget.sendViewModel.currency.toString()}:',
child: Text('XMR:', style: TextStyle(
style: TextStyle( fontSize: 16,
fontSize: 16, fontWeight: FontWeight.w500,
fontWeight: FontWeight.w500, color: Theme.of(context)
.primaryTextTheme
.title
.color,
)),
),
suffixIcon: Padding(
padding: EdgeInsets.only(bottom: 5),
child: Container(
height: 32,
width: 32,
margin: EdgeInsets.only(
left: 12, bottom: 7, top: 4),
decoration: BoxDecoration(
color: Theme.of(context) color: Theme.of(context)
.primaryTextTheme .accentTextTheme
.title .title
.color, .color,
)), borderRadius:
), BorderRadius.all(Radius.circular(6))),
suffixIcon: Padding( child: InkWell(
padding: EdgeInsets.only(bottom: 5), onTap: () => widget.sendViewModel.setAll(),
child: Row( child: Center(
mainAxisSize: MainAxisSize.min, child: Text(S.of(context).all,
mainAxisAlignment: textAlign: TextAlign.center,
MainAxisAlignment.spaceBetween,
children: <Widget>[
Container(
width:
MediaQuery.of(context).size.width / 2,
alignment: Alignment.centerLeft,
child: Text(
' / ' + widget.sendViewModel.balance,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 9,
fontWeight: FontWeight.bold,
color: Theme.of(context) color: Theme.of(context)
.primaryTextTheme .primaryTextTheme
.caption .caption
.color)), .color)),
), ),
Container( ),
height: 32, )),
width: 32, hintStyle: TextStyle(
margin: EdgeInsets.only( fontSize: 16.0,
left: 12, bottom: 7, top: 4), color: Theme.of(context)
decoration: BoxDecoration( .primaryTextTheme
color: Theme.of(context) .title
.accentTextTheme .color),
.title hintText: '0.0000',
.color, focusedBorder: UnderlineInputBorder(
borderRadius: BorderRadius.all( borderSide: BorderSide(
Radius.circular(6))), color: Theme.of(context).dividerColor,
child: InkWell( width: 1.0)),
onTap: () => null, enabledBorder: UnderlineInputBorder(
// widget.sendViewModel, borderSide: BorderSide(
child: Center( color: Theme.of(context).dividerColor,
child: Text(S.of(context).all, width: 1.0))),
textAlign: TextAlign.center, validator: widget.sendViewModel.amountValidator),
style: TextStyle( ),
fontSize: 9,
fontWeight: FontWeight.bold,
color: Theme.of(context)
.primaryTextTheme
.caption
.color)),
),
),
)
],
),
),
hintStyle: TextStyle(
fontSize: 16.0,
color: Theme.of(context)
.primaryTextTheme
.title
.color),
hintText: '0.0000',
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).dividerColor,
width: 1.0)),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).dividerColor,
width: 1.0))),
validator: widget.sendViewModel.amountValidator),
);
}),
Padding( Padding(
padding: const EdgeInsets.only(top: 20), padding: const EdgeInsets.only(top: 20),
child: TextFormField( child: TextFormField(
onChanged: (value) =>
widget.sendViewModel.setFiatAmount(value),
style: TextStyle( style: TextStyle(
fontSize: 16.0, fontSize: 16.0,
color: color:
@ -274,10 +227,10 @@ class SendFormState extends State<SendForm> {
controller: _fiatAmountController, controller: _fiatAmountController,
keyboardType: TextInputType.numberWithOptions( keyboardType: TextInputType.numberWithOptions(
signed: false, decimal: true), signed: false, decimal: true),
inputFormatters: [ // inputFormatters: [
BlacklistingTextInputFormatter( // BlacklistingTextInputFormatter(
RegExp('[\\-|\\ |\\,]')) // RegExp('[\\-|\\ |\\,]'))
], // ],
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: Padding( prefixIcon: Padding(
padding: EdgeInsets.only(top: 12), padding: EdgeInsets.only(top: 12),
@ -426,52 +379,43 @@ class SendFormState extends State<SendForm> {
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: Observer(builder: (_) { bottomSection: Observer(builder: (_) {
return LoadingPrimaryButton( return LoadingPrimaryButton(
onPressed: () => null, onPressed: () async {
// syncStore.status is SyncedSyncStatus FocusScope.of(context).requestFocus(FocusNode());
// ? () async {
// // Hack. Don't ask me. if (!_formKey.currentState.validate()) {
// FocusScope.of(context).requestFocus(FocusNode()); return;
// }
// if (_formKey.currentState.validate()) {
// await showDialog<void>( await showDialog<void>(
// context: context, context: context,
// builder: (dialogContext) { builder: (dialogContext) {
// return AlertWithTwoActions( return AlertWithTwoActions(
// alertTitle: alertTitle: S.of(context).send_creating_transaction,
// S.of(context).send_creating_transaction, alertContent: S.of(context).confirm_sending,
// alertContent: S.of(context).confirm_sending, leftButtonText: S.of(context).send,
// leftButtonText: S.of(context).send, rightButtonText: S.of(context).cancel,
// rightButtonText: S.of(context).cancel, actionLeftButton: () async {
// actionLeftButton: () async { await Navigator.of(dialogContext)
// await Navigator.of(dialogContext) .popAndPushNamed(Routes.auth, arguments:
// .popAndPushNamed(Routes.auth, arguments: (bool isAuthenticatedSuccessfully,
// (bool isAuthenticatedSuccessfully, AuthPageState auth) {
// AuthPageState auth) { if (!isAuthenticatedSuccessfully) {
// if (!isAuthenticatedSuccessfully) { return;
// return; }
// }
// Navigator.of(auth.context).pop();
// Navigator.of(auth.context).pop(); widget.sendViewModel.createTransaction();
// });
// sendStore.createTransaction( },
// address: _addressController.text, actionRightButton: () => Navigator.of(context).pop());
// paymentId: ''); });
// }); },
// },
// actionRightButton: () =>
// Navigator.of(context).pop());
// });
// }
// }
// : null,
text: S.of(context).send, text: S.of(context).send,
color: Colors.blue, color: Colors.blue,
textColor: Colors.white, textColor: Colors.white,
isLoading: widget.sendViewModel.state is TransactionIsCreating || isLoading: widget.sendViewModel.state is TransactionIsCreating ||
widget.sendViewModel.state is TransactionCommitting, widget.sendViewModel.state is TransactionCommitting,
isDisabled: isDisabled: !widget.sendViewModel.isReadyForSend);
false // FIXME !(syncStore.status is SyncedSyncStatus),
);
}), }),
), ),
); );
@ -482,47 +426,42 @@ class SendFormState extends State<SendForm> {
return; return;
} }
// reaction((_) => widget.sendViewModel.fiatAmount, (String amount) { reaction((_) => widget.sendViewModel.all, (bool all) {
// if (amount != _fiatAmountController.text) { if (all) {
// _fiatAmountController.text = amount; _cryptoAmountController.text = S.current.all;
// } _fiatAmountController.text = null;
// }); }
// });
// reaction((_) => widget.sendViewModel.cryptoAmount, (String amount) {
// if (amount != _cryptoAmountController.text) {
// _cryptoAmountController.text = amount;
// }
// });
//
// reaction((_) => widget.sendViewModel.address, (String address) {
// if (address != _addressController.text) {
// _addressController.text = address;
// }
// });
//
// _addressController.addListener(() {
// final address = _addressController.text;
//
// if (widget.sendViewModel.address != address) {
// widget.sendViewModel.changeAddress(address);
// }
// });
// _fiatAmountController.addListener(() { reaction((_) => widget.sendViewModel.fiatAmount, (String amount) {
// final fiatAmount = _fiatAmountController.text; if (amount != _fiatAmountController.text) {
// _fiatAmountController.text = amount;
// if (sendStore.fiatAmount != fiatAmount) { }
// sendStore.changeFiatAmount(fiatAmount); });
// }
// });
// _cryptoAmountController.addListener(() { reaction((_) => widget.sendViewModel.cryptoAmount, (String amount) {
// final cryptoAmount = _cryptoAmountController.text; if (widget.sendViewModel.all && amount != S.current.all) {
// widget.sendViewModel.all = false;
// if (sendStore.cryptoAmount != cryptoAmount) { }
// sendStore.changeCryptoAmount(cryptoAmount);
// } if (amount != _cryptoAmountController.text) {
// }); _cryptoAmountController.text = amount;
}
});
reaction((_) => widget.sendViewModel.address, (String address) {
if (address != _addressController.text) {
_addressController.text = address;
}
});
_addressController.addListener(() {
final address = _addressController.text;
if (widget.sendViewModel.address != address) {
widget.sendViewModel.address = address;
}
});
reaction((_) => widget.sendViewModel.state, (SendViewModelState state) { reaction((_) => widget.sendViewModel.state, (SendViewModelState state) {
if (state is SendingFailed) { if (state is SendingFailed) {
@ -540,30 +479,117 @@ class SendFormState extends State<SendForm> {
} }
if (state is TransactionCreatedSuccessfully) { if (state is TransactionCreatedSuccessfully) {
// WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
// showDialog<void>( showDialog<void>(
// context: context, context: context,
// builder: (BuildContext context) { builder: (BuildContext context) {
// return ConfirmSendingAlert( return ConfirmSendingAlert(
// alertTitle: S.of(context).confirm_sending, alertTitle: S.of(context).confirm_sending,
// amount: S.of(context).send_amount, amount: S.of(context).send_amount,
// amountValue: sendStore.pendingTransaction.amount, amountValue:
// fee: S.of(context).send_fee, widget.sendViewModel.pendingTransaction.amountFormatted,
// feeValue: sendStore.pendingTransaction.fee, fee: S.of(context).send_fee,
// leftButtonText: S.of(context).ok, feeValue:
// rightButtonText: S.of(context).cancel, widget.sendViewModel.pendingTransaction.feeFormatted,
// actionLeftButton: () { leftButtonText: S.of(context).ok,
// Navigator.of(context).pop(); rightButtonText: S.of(context).cancel,
// sendStore.commitTransaction(); actionLeftButton: () {
// showDialog<void>( Navigator.of(context).pop();
// context: context, widget.sendViewModel.commitTransaction();
// builder: (BuildContext context) { showDialog<void>(
// return SendingAlert(sendStore: sendStore); context: context,
// }); builder: (BuildContext context) {
// }, return Observer(builder: (_) {
// actionRightButton: () => Navigator.of(context).pop()); final state = widget.sendViewModel.state;
// });
// }); if (state is TransactionCommitted) {
return Stack(
children: <Widget>[
Container(
color: Theme.of(context).backgroundColor,
child: Center(
child: Image.asset(
'assets/images/birthday_cake.png'),
),
),
Center(
child: Padding(
padding: EdgeInsets.only(
top: 220, left: 24, right: 24),
child: Text(
S.of(context).send_success,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Theme.of(context)
.primaryTextTheme
.title
.color,
decoration: TextDecoration.none,
),
),
),
),
Positioned(
left: 24,
right: 24,
bottom: 24,
child: PrimaryButton(
onPressed: () =>
Navigator.of(context).pop(),
text: S.of(context).send_got_it,
color: Colors.blue,
textColor: Colors.white))
],
);
}
return Stack(
children: <Widget>[
Container(
color: Theme.of(context).backgroundColor,
child: Center(
child: Image.asset(
'assets/images/birthday_cake.png'),
),
),
BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 3.0, sigmaY: 3.0),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context)
.backgroundColor
.withOpacity(0.25)),
child: Center(
child: Padding(
padding: EdgeInsets.only(top: 220),
child: Text(
S.of(context).send_sending,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Theme.of(context)
.primaryTextTheme
.title
.color,
decoration: TextDecoration.none,
),
),
),
),
),
)
],
);
});
});
},
actionRightButton: () => Navigator.of(context).pop());
});
});
} }
if (state is TransactionCommitted) { if (state is TransactionCommitted) {

View file

@ -1,6 +1,6 @@
import 'dart:ui'; import 'dart:ui';
import 'package:cake_wallet/src/stores/send/sending_state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cake_wallet/src/stores/send/sending_state.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/stores/send/send_store.dart'; import 'package:cake_wallet/src/stores/send/send_store.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';

View file

@ -39,9 +39,11 @@ class SettingsPage extends BasePage {
if (item is PickerListItem) { if (item is PickerListItem) {
return Observer(builder: (_) { return Observer(builder: (_) {
return SettingsPickerCell<dynamic>( return SettingsPickerCell<dynamic>(
title: item.title, title: item.title,
selectedItem: item.selectedItem(), selectedItem: item.selectedItem(),
items: item.items); items: item.items,
onItemSelected: (dynamic value) => item.onItemSelected(value),
);
}); });
} }

View file

@ -4,25 +4,30 @@ import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
class SettingsPickerCell<ItemType> extends StandardListRow { class SettingsPickerCell<ItemType> extends StandardListRow {
SettingsPickerCell({@required String title, this.selectedItem, this.items}) SettingsPickerCell(
{@required String title,
this.selectedItem,
this.items,
this.onItemSelected})
: super( : super(
title: title, title: title,
isSelected: false, isSelected: false,
onTap: (BuildContext context) async { onTap: (BuildContext context) async {
final selectedAtIndex = items.indexOf(selectedItem); final selectedAtIndex = items.indexOf(selectedItem);
await showDialog<void>( await showDialog<void>(
context: context, context: context,
builder: (_) => Picker( builder: (_) => Picker(
items: items, items: items,
selectedAtIndex: selectedAtIndex, selectedAtIndex: selectedAtIndex,
title: S.current.please_select, title: S.current.please_select,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
onItemSelected: (Object _) {})); onItemSelected: (ItemType item) => onItemSelected?.call(item)));
}); });
final ItemType selectedItem; final ItemType selectedItem;
final List<ItemType> items; final List<ItemType> items;
final void Function(ItemType item) onItemSelected;
@override @override
Widget buildTrailing(BuildContext context) { Widget buildTrailing(BuildContext context) {
@ -35,4 +40,4 @@ class SettingsPickerCell<ItemType> extends StandardListRow {
color: Theme.of(context).primaryTextTheme.caption.color), color: Theme.of(context).primaryTextTheme.caption.color),
); );
} }
} }

View file

@ -42,12 +42,6 @@ abstract class SettingsStoreBase with Store {
languageCode = initialLanguageCode; languageCode = initialLanguageCode;
currentLocale = initialCurrentLocale; currentLocale = initialCurrentLocale;
itemHeaders = {}; itemHeaders = {};
// actionlistDisplayMode.observe(
// (dynamic _) => _sharedPreferences.setInt(displayActionListModeKey,
// serializeActionlistDisplayModes(actionlistDisplayMode)),
// fireImmediately: false);
_sharedPreferences = sharedPreferences; _sharedPreferences = sharedPreferences;
_nodeSource = nodeSource; _nodeSource = nodeSource;
} }
@ -120,7 +114,7 @@ abstract class SettingsStoreBase with Store {
sharedPreferences.getBool(shouldSaveRecipientAddressKey); sharedPreferences.getBool(shouldSaveRecipientAddressKey);
final allowBiometricalAuthentication = final allowBiometricalAuthentication =
sharedPreferences.getBool(allowBiometricalAuthenticationKey) ?? false; sharedPreferences.getBool(allowBiometricalAuthenticationKey) ?? false;
final savedDarkTheme = sharedPreferences.getBool(currentDarkTheme) ?? false; final savedDarkTheme = sharedPreferences.getBool(currentDarkTheme) ?? true;
final actionListDisplayMode = ObservableList<ActionListDisplayMode>(); final actionListDisplayMode = ObservableList<ActionListDisplayMode>();
actionListDisplayMode.addAll(deserializeActionlistDisplayModes( actionListDisplayMode.addAll(deserializeActionlistDisplayModes(
sharedPreferences.getInt(displayActionListModeKey) ?? sharedPreferences.getInt(displayActionListModeKey) ??

View file

@ -35,9 +35,11 @@ abstract class BalanceViewModelBase with Store {
if (_wallet is BitcoinWallet) { if (_wallet is BitcoinWallet) {
return WalletBalance( return WalletBalance(
unlockedBalance: _wallet.balance.confirmedFormatted, unlockedBalance: _wallet.balance.totalFormatted,
totalBalance: _wallet.balance.unconfirmedFormatted); totalBalance: _wallet.balance.totalFormatted);
} }
return null;
} }
String _getFiatBalance({double price, String cryptoAmount}) { String _getFiatBalance({double price, String cryptoAmount}) {

View file

@ -29,24 +29,24 @@ part 'dashboard_view_model.g.dart';
class DashboardViewModel = DashboardViewModelBase with _$DashboardViewModel; class DashboardViewModel = DashboardViewModelBase with _$DashboardViewModel;
abstract class DashboardViewModelBase with Store { abstract class DashboardViewModelBase with Store {
DashboardViewModelBase({ DashboardViewModelBase(
this.balanceViewModel, {this.balanceViewModel,
this.appStore, this.appStore,
this.tradesStore, this.tradesStore,
this.tradeFilterStore, this.tradeFilterStore,
this.transactionFilterStore, this.transactionFilterStore,
this.pageViewStore}) { this.pageViewStore}) {
name = appStore.wallet?.name; name = appStore.wallet?.name;
wallet ??= appStore.wallet; wallet ??= appStore.wallet;
type = wallet.type; type = wallet.type;
transactions = ObservableList.of(wallet.transactionHistory.transactions transactions = ObservableList.of(wallet
.transactionHistory.transactions.values
.map((transaction) => TransactionListItem( .map((transaction) => TransactionListItem(
transaction: transaction, transaction: transaction,
price: price, price: price,
fiatCurrency: appStore.settingsStore.fiatCurrency, fiatCurrency: appStore.settingsStore.fiatCurrency,
displayMode: balanceDisplayMode))); displayMode: balanceDisplayMode)));
_reaction = reaction((_) => appStore.wallet, _onWalletChange); _reaction = reaction((_) => appStore.wallet, _onWalletChange);
@ -83,15 +83,11 @@ abstract class DashboardViewModelBase with Store {
var statusText = ''; var statusText = '';
if (status is SyncingSyncStatus) { if (status is SyncingSyncStatus) {
statusText = S.current statusText = S.current.Blocks_remaining(status.toString());
.Blocks_remaining(
status.toString());
} }
if (status is FailedSyncStatus) { if (status is FailedSyncStatus) {
statusText = S statusText = S.current.please_try_to_connect_to_another_node;
.current
.please_try_to_connect_to_another_node;
} }
return statusText; return statusText;
@ -111,8 +107,7 @@ abstract class DashboardViewModelBase with Store {
List<ActionListItem> get items { List<ActionListItem> get items {
final _items = <ActionListItem>[]; final _items = <ActionListItem>[];
_items _items.addAll(transactionFilterStore.filtered(transactions: transactions));
.addAll(transactionFilterStore.filtered(transactions: transactions));
_items.addAll(tradeFilterStore.filtered(trades: trades)); _items.addAll(tradeFilterStore.filtered(trades: trades));
return formattedItemsList(_items); return formattedItemsList(_items);
@ -137,11 +132,11 @@ abstract class DashboardViewModelBase with Store {
void _onWalletChange(WalletBase wallet) { void _onWalletChange(WalletBase wallet) {
name = wallet.name; name = wallet.name;
transactions.clear(); transactions.clear();
transactions.addAll(wallet.transactionHistory.transactions transactions.addAll(wallet.transactionHistory.transactions.values.map(
.map((transaction) => TransactionListItem( (transaction) => TransactionListItem(
transaction: transaction, transaction: transaction,
price: price, price: price,
fiatCurrency: appStore.settingsStore.fiatCurrency, fiatCurrency: appStore.settingsStore.fiatCurrency,
displayMode: balanceDisplayMode))); displayMode: balanceDisplayMode)));
} }
} }

View file

@ -0,0 +1,171 @@
import 'package:cake_wallet/src/domain/common/calculate_fiat_amount.dart';
import 'package:cake_wallet/store/dashboard/fiat_convertation_store.dart';
import 'package:intl/intl.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/core/amount_validator.dart';
import 'package:cake_wallet/core/pending_transaction.dart';
import 'package:cake_wallet/core/validator.dart';
import 'package:cake_wallet/core/wallet_base.dart';
import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
import 'package:cake_wallet/monero/monero_wallet.dart';
import 'package:cake_wallet/src/domain/common/sync_status.dart';
import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
import 'package:cake_wallet/src/domain/common/fiat_currency.dart';
import 'package:cake_wallet/src/domain/common/transaction_priority.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart';
part 'send_view_model.g.dart';
class SendViewModel = SendViewModelBase with _$SendViewModel;
abstract class SendViewModelBase with Store {
SendViewModelBase(
this._wallet, this._settingsStore, this._fiatConversationStore)
: state = InitialSendViewModelState(),
_cryptoNumberFormat = NumberFormat()..maximumFractionDigits = 12,
all = false;
@observable
SendViewModelState state;
@observable
String fiatAmount;
@observable
String cryptoAmount;
@observable
String address;
@observable
bool all;
FiatCurrency get fiat => _settingsStore.fiatCurrency;
TransactionPriority get transactionPriority =>
_settingsStore.transactionPriority;
double get estimatedFee =>
_wallet.calculateEstimatedFee(_settingsStore.transactionPriority);
CryptoCurrency get currency => _wallet.currency;
Validator get amountValidator => AmountValidator(type: _wallet.type);
Validator get addressValidator => AddressValidator(type: _wallet.currency);
PendingTransaction pendingTransaction;
@computed
String get balance {
if (_wallet is MoneroWallet) {
_wallet.balance.formattedUnlockedBalance;
}
if (_wallet is BitcoinWallet) {
_wallet.balance.confirmedFormatted;
}
return '0.0';
}
@computed
bool get isReadyForSend => _wallet.syncStatus is SyncedSyncStatus;
final WalletBase _wallet;
final SettingsStore _settingsStore;
final FiatConvertationStore _fiatConversationStore;
NumberFormat _cryptoNumberFormat;
@action
void setAll() => all = true;
@action
void reset() {
cryptoAmount = '';
fiatAmount = '';
address = '';
}
@action
Future<void> createTransaction() async {
try {
state = TransactionIsCreating();
pendingTransaction = await _wallet.createTransaction(_credentials());
state = TransactionCreatedSuccessfully();
} catch (e) {
state = SendingFailed(error: e.toString());
}
}
@action
Future<void> commitTransaction() async {
try {
state = TransactionCommitting();
await pendingTransaction.commit();
state = TransactionCommitted();
} catch (e) {
state = SendingFailed(error: e.toString());
}
}
@action
void setCryptoAmount(String amount) {
cryptoAmount = amount;
_updateFiatAmount();
}
@action
void setFiatAmount(String amount) {
fiatAmount = amount;
_updateCryptoAmount();
}
@action
void _updateFiatAmount() {
try {
final fiat = calculateFiatAmount(
price: _fiatConversationStore.price, cryptoAmount: cryptoAmount);
if (fiatAmount != fiat) {
fiatAmount = fiat;
}
} catch (_) {
fiatAmount = '';
}
}
@action
void _updateCryptoAmount() {
try {
final crypto = double.parse(fiatAmount) / _fiatConversationStore.price;
final cryptoAmountTmp = _cryptoNumberFormat.format(crypto);
if (cryptoAmount != cryptoAmountTmp) {
cryptoAmount = cryptoAmountTmp;
}
} catch (e) {
cryptoAmount = '';
}
}
Object _credentials() {
final amount =
!all ? double.parse(cryptoAmount.replaceAll(',', '.')) : null;
switch (_wallet.type) {
case WalletType.bitcoin:
return BitcoinTransactionCredentials(
address, amount, _settingsStore.transactionPriority);
case WalletType.monero:
// FIXME: Wrong credentials
return BitcoinTransactionCredentials(
address, amount, _settingsStore.transactionPriority);
default:
return null;
}
}
}

View file

@ -0,0 +1,18 @@
import 'package:flutter/foundation.dart';
abstract class SendViewModelState {}
class InitialSendViewModelState extends SendViewModelState {}
class TransactionIsCreating extends SendViewModelState {}
class TransactionCreatedSuccessfully extends SendViewModelState {}
class TransactionCommitting extends SendViewModelState {}
class TransactionCommitted extends SendViewModelState {}
class SendingFailed extends SendViewModelState {
SendingFailed({@required this.error});
String error;
}

View file

@ -1,94 +0,0 @@
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/core/amount_validator.dart';
import 'package:cake_wallet/core/validator.dart';
import 'package:cake_wallet/core/wallet_base.dart';
import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
import 'package:cake_wallet/monero/monero_wallet.dart';
import 'package:cake_wallet/src/domain/common/balance.dart';
import 'package:cake_wallet/src/domain/common/calculate_estimated_fee.dart';
import 'package:cake_wallet/src/domain/common/crypto_currency.dart';
import 'package:cake_wallet/src/domain/common/fiat_currency.dart';
import 'package:cake_wallet/src/domain/common/transaction_priority.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/monero/monero_wallet_service.dart';
import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart';
import 'package:cake_wallet/core/wallet_creation_service.dart';
import 'package:cake_wallet/core/wallet_credentials.dart';
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:cake_wallet/view_model/wallet_creation_vm.dart';
part 'send_view_model.g.dart';
abstract class SendViewModelState {}
class InitialSendViewModelState extends SendViewModelState {}
class TransactionIsCreating extends SendViewModelState {}
class TransactionCreatedSuccessfully extends SendViewModelState {}
class TransactionCommitting extends SendViewModelState {}
class TransactionCommitted extends SendViewModelState {}
class SendingFailed extends SendViewModelState {
SendingFailed({@required this.error});
String error;
}
class SendViewModel = SendViewModelBase with _$SendViewModel;
abstract class SendViewModelBase with Store {
SendViewModelBase(this._wallet, this._settingsStore)
: state = InitialSendViewModelState();
@observable
SendViewModelState state;
@observable
String fiatAmount;
@observable
String cryptoAmount;
@observable
String address;
FiatCurrency get fiat => _settingsStore.fiatCurrency;
TransactionPriority get transactionPriority =>
_settingsStore.transactionPriority;
double get estimatedFee =>
calculateEstimatedFee(priority: transactionPriority);
CryptoCurrency get currency => _wallet.currency;
Validator get amountValidator => AmountValidator(type: _wallet.type);
Validator get addressValidator => AddressValidator(type: _wallet.currency);
@computed
String get balance {
if (_wallet is MoneroWallet) {
_wallet.balance.formattedUnlockedBalance;
}
if (_wallet is BitcoinWallet) {
_wallet.balance.confirmedFormatted;
}
return '0.0';
}
WalletBase _wallet;
SettingsStore _settingsStore;
Future<void> createTransaction() async {}
Future<void> commitTransaction() async {}
}

View file

@ -4,10 +4,19 @@ import 'package:cake_wallet/view_model/settings/settings_list_item.dart';
class PickerListItem<ItemType> extends SettingsListItem { class PickerListItem<ItemType> extends SettingsListItem {
PickerListItem( PickerListItem(
{@required String title, {@required String title,
@required this.selectedItem, @required this.selectedItem,
@required this.items}) @required this.items,
: super(title); void Function(ItemType item) onItemSelected})
: _onItemSelected = onItemSelected,
super(title);
final ItemType Function() selectedItem; final ItemType Function() selectedItem;
final List<ItemType> items; final List<ItemType> items;
final void Function(ItemType item) _onItemSelected;
void onItemSelected(dynamic item) {
if (item is ItemType) {
_onItemSelected?.call(item);
}
}
} }

View file

@ -1,3 +1,5 @@
import 'package:cake_wallet/core/wallet_base.dart';
import 'package:cake_wallet/src/domain/common/wallet_type.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
@ -20,7 +22,8 @@ part 'settings_view_model.g.dart';
class SettingsViewModel = SettingsViewModelBase with _$SettingsViewModel; class SettingsViewModel = SettingsViewModelBase with _$SettingsViewModel;
abstract class SettingsViewModelBase with Store { abstract class SettingsViewModelBase with Store {
SettingsViewModelBase(this._settingsStore) : itemHeaders = {} { SettingsViewModelBase(this._settingsStore, WalletBase wallet)
: itemHeaders = {} {
sections = [ sections = [
[ [
PickerListItem( PickerListItem(
@ -33,8 +36,10 @@ abstract class SettingsViewModelBase with Store {
selectedItem: () => fiatCurrency), selectedItem: () => fiatCurrency),
PickerListItem( PickerListItem(
title: S.current.settings_fee_priority, title: S.current.settings_fee_priority,
items: TransactionPriority.all, items: _transactionPriorities(wallet.type),
selectedItem: () => transactionPriority), selectedItem: () => transactionPriority,
onItemSelected: (TransactionPriority priority) =>
_settingsStore.transactionPriority = priority),
SwitcherListItem( SwitcherListItem(
title: S.current.settings_save_recipient_address, title: S.current.settings_save_recipient_address,
value: () => shouldSaveRecipientAddress, value: () => shouldSaveRecipientAddress,
@ -146,12 +151,9 @@ abstract class SettingsViewModelBase with Store {
_settingsStore.allowBiometricalAuthentication = value; _settingsStore.allowBiometricalAuthentication = value;
// @observable // @observable
// bool isDarkTheme;
//
// @observable
// int defaultPinLength;
// @observable // @observable
final Map<String, String> itemHeaders; final Map<String, String> itemHeaders;
List<List<SettingsListItem>> sections; List<List<SettingsListItem>> sections;
final SettingsStore _settingsStore; final SettingsStore _settingsStore;
@ -182,4 +184,24 @@ abstract class SettingsViewModelBase with Store {
@action @action
void _showTrades() => actionlistDisplayMode.add(ActionListDisplayMode.trades); void _showTrades() => actionlistDisplayMode.add(ActionListDisplayMode.trades);
//
// @observable
// int defaultPinLength;
// bool isDarkTheme;
static List<TransactionPriority> _transactionPriorities(WalletType type) {
switch (type) {
case WalletType.monero:
return TransactionPriority.all;
case WalletType.bitcoin:
return [
TransactionPriority.slow,
TransactionPriority.regular,
TransactionPriority.fast
];
default:
return [];
}
}
} }