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

2
.gitignore vendored
View file

@ -90,3 +90,5 @@ android/key.properties
**/lib/.secrets.g.dart
vendor/
android/app/.cxx/**

View file

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

View file

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

View file

@ -1,17 +1,19 @@
import 'dart:convert';
class BitcoinAddressRecord {
BitcoinAddressRecord(this.address, {this.label});
BitcoinAddressRecord(this.address, {this.label, this.index});
factory BitcoinAddressRecord.fromJSON(String jsonSource) {
final decoded = json.decode(jsonSource) as Map;
return BitcoinAddressRecord(decoded['address'] as String,
label: decoded['label'] as String);
label: decoded['label'] as String, index: decoded['index'] as int);
}
final String address;
int index;
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 {
const BitcoinTransactionCredentials(this.address, this.amount);
BitcoinTransactionCredentials(this.address, this.amount, this.priority);
final String address;
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_transaction_info.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';
@ -24,100 +22,176 @@ abstract class BitcoinTransactionHistoryBase
{this.eclient, String dirPath, @required String password})
: path = '$dirPath/$_transactionsHistoryFileName',
_password = password,
_height = 0;
_height = 0,
_isUpdating = false {
transactions = ObservableMap<String, BitcoinTransactionInfo>();
}
BitcoinWalletBase wallet;
final ElectrumClient eclient;
final String path;
final String _password;
int _height;
bool _isUpdating;
Future<void> init() async {
final info = await _read();
_height = info['height'] as int ?? _height;
transactions = ObservableList.of(
info['transactions'] as List<BitcoinTransactionInfo> ??
<BitcoinTransactionInfo>[]);
await _load();
}
@override
Future update() async {
await super.update();
_updateHeight();
if (_isUpdating) {
return;
}
try {
_isUpdating = true;
final txs = await fetchTransactions();
await add(txs);
_isUpdating = false;
} catch (_) {
_isUpdating = false;
rethrow;
}
}
@override
Future<List<BitcoinTransactionInfo>> fetchTransactions() async {
final addresses = wallet.addresses;
Future<Map<String, BitcoinTransactionInfo>> fetchTransactions() async {
final histories =
addresses.map((record) => eclient.getHistory(address: record.address));
wallet.scriptHashes.map((scriptHash) => eclient.getHistory(scriptHash));
final _historiesWithDetails = await Future.wait(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)
.toList())
.then((histories) => histories.map((tx) => fetchTransactionInfo(
hash: tx['tx_hash'] as String, height: tx['height'] as int)));
final historiesWithDetails = await Future.wait(_historiesWithDetails);
return historiesWithDetails
.map((info) => BitcoinTransactionInfo.fromHexAndHeader(
info['raw'] as String, info['header'] as Map<String, Object>,
addresses: addresses.map((record) => record.address).toList()))
.toList();
return historiesWithDetails.fold<Map<String, BitcoinTransactionInfo>>(
<String, BitcoinTransactionInfo>{}, (acc, tx) {
acc[tx.id] = tx;
return acc;
});
}
Future<Map<String, Object>> fetchTransactionInfo(
Future<BitcoinTransactionInfo> fetchTransactionInfo(
{@required String hash, @required int height}) async {
final rawFetching = eclient.getTransactionRaw(hash: hash);
final headerFetching = eclient.getHeader(height: height);
final result = await Future.wait([rawFetching, headerFetching]);
final raw = result.first as String;
final header = result[1] as Map<String, Object>;
return {'raw': raw, 'header': header};
final tx = await eclient.getTransactionExpanded(hash: hash);
return BitcoinTransactionInfo.fromElectrumVerbose(tx,
height: height, addresses: wallet.addresses);
}
Future<void> add(List<BitcoinTransactionInfo> transactions) async {
this.transactions.addAll(transactions);
Future<void> add(Map<String, BitcoinTransactionInfo> transactionsList) async {
transactionsList.entries.forEach((entry) {
_updateOrInsert(entry.value);
if (entry.value.height > _height) {
_height = entry.value.height;
}
});
await save();
}
Future<void> addOne(BitcoinTransactionInfo tx) async {
transactions.add(tx);
_updateOrInsert(tx);
if (tx.height > _height) {
_height = tx.height;
}
await save();
}
Future<void> save() async => writeData(
path: path,
password: _password,
data: json.encode({'height': _height, 'transactions': transactions}));
BitcoinTransactionInfo get(String id) => transactions[id];
Future<void> save() async {
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 {
try {
final content = await read(path: path, password: _password);
final jsoned = json.decode(content) 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 json.decode(content) as Map<String, Object>;
}
return null;
})
.where((el) => el != null)
.toList();
Future<void> _load() async {
try {
final content = await _read();
final txs = content['transactions'] as Map<String, Object> ?? {};
return {'transactions': transactions, 'height': height};
} catch (_) {
return {'transactions': <BitcoinTransactionInfo>[], 'height': 0};
txs.entries.forEach((entry) {
final val = entry.value;
if (val is Map<String, Object>) {
final tx = BitcoinTransactionInfo.fromJson(val);
_updateOrInsert(tx);
}
});
_height = content['height'] as int;
} catch (_) {}
}
void _updateHeight() {
final newHeight = transactions.fold(
0, (int acc, val) => val.height > acc ? val.height : acc);
_height = newHeight > _height ? newHeight : _height;
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;
}
}
}

View file

@ -1,7 +1,9 @@
import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
import 'package:flutter/foundation.dart';
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: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_info.dart';
import 'package:cake_wallet/src/domain/common/format_amount.dart';
@ -13,7 +15,8 @@ class BitcoinTransactionInfo extends TransactionInfo {
@required int amount,
@required TransactionDirection direction,
@required bool isPending,
@required DateTime date}) {
@required DateTime date,
@required this.confirmations}) {
this.height = height;
this.amount = amount;
this.direction = direction;
@ -21,13 +24,61 @@ class BitcoinTransactionInfo extends TransactionInfo {
this.isPending = isPending;
}
factory BitcoinTransactionInfo.fromHexAndHeader(
String hex, Map<String, Object> header,
{List<String> addresses}) {
factory BitcoinTransactionInfo.fromElectrumVerbose(Map<String, Object> obj,
{@required List<BitcoinAddressRecord> addresses, @required int height}) {
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);
var exist = false;
var amount = 0;
if (addresses != null) {
tx.outs.forEach((out) {
try {
final p2pkh = bitcoin.P2PKH(
@ -39,16 +90,21 @@ class BitcoinTransactionInfo extends TransactionInfo {
}
} catch (_) {}
});
}
final date = timestamp != null
? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000)
: DateTime.now();
// FIXME: Get transaction is pending
return BitcoinTransactionInfo(
id: tx.getId(),
height: header['block_height'] as int,
height: height,
isPending: false,
direction: TransactionDirection.incoming,
amount: amount,
date: DateTime.fromMillisecondsSinceEpoch(
(header['timestamp'] as int) * 1000));
date: date,
confirmations: confirmations);
}
factory BitcoinTransactionInfo.fromJson(Map<String, dynamic> data) {
@ -58,15 +114,18 @@ class BitcoinTransactionInfo extends TransactionInfo {
amount: data['amount'] as int,
direction: parseTransactionDirectionFromInt(data['direction'] 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;
int confirmations;
String _fiatAmount;
@override
String amountFormatted() => '${formatAmount(bitcoinAmountToString(amount: amount))} BTC';
String amountFormatted() =>
'${formatAmount(bitcoinAmountToString(amount: amount))} BTC';
@override
String fiatAmount() => _fiatAmount ?? '';
@ -75,13 +134,14 @@ class BitcoinTransactionInfo extends TransactionInfo {
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
Map<String, dynamic> toJson() {
final m = Map<String, dynamic>();
final m = <String, dynamic>{};
m['id'] = id;
m['height'] = height;
m['amount'] = amount;
m['direction'] = direction.index;
m['date'] = date.millisecondsSinceEpoch;
m['isPending'] = isPending;
m['confirmations'] = confirmations;
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:convert';
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/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/common/crypto_currency.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:mobx/mobx.dart';
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/core/wallet_base.dart';
import 'package:rxdart/rxdart.dart';
import 'package:hex/hex.dart';
part 'bitcoin_wallet.g.dart';
class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet;
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(
{@required String password,
@required String name,
@ -37,12 +74,12 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
(data['account_index'] == 'null' || data['account_index'] == null)
? 0
: int.parse(data['account_index'] as String);
final _addresses = data['addresses'] as List;
final _addresses = data['addresses'] as List ?? <Object>[];
final addresses = <BitcoinAddressRecord>[];
final balance = BitcoinBalance.fromJSON(data['balance'] as String) ??
BitcoinBalance(confirmed: 0, unconfirmed: 0);
_addresses?.forEach((Object el) {
_addresses.forEach((Object el) {
if (el is String) {
addresses.add(BitcoinAddressRecord.fromJSON(el));
}
@ -83,34 +120,10 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
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
final BitcoinTransactionHistory transactionHistory;
final String path;
bitcoin.HDWallet hd;
final bitcoin.HDWallet hd;
final ElectrumClient eclient;
final String mnemonic;
@ -131,6 +144,11 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
ObservableList<BitcoinAddressRecord> addresses;
Map<String, bitcoin.ECPair> _addressesKeys;
List<String> get scriptHashes =>
addresses.map((addr) => scriptHash(addr.address)).toList();
String get xpub => hd.base58;
@override
@ -142,11 +160,13 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
int _accountIndex;
String _password;
BehaviorSubject<Object> _addressUpdateSubject;
Map<String, BehaviorSubject<Object>> _scripthashesUpdateSubject;
Future<void> init() async {
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;
@ -156,9 +176,8 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
Future<BitcoinAddressRecord> generateNewAddress({String label}) async {
_accountIndex += 1;
final address = BitcoinAddressRecord(
_getAddress(hd: hd, index: _accountIndex),
label: label);
final address = BitcoinAddressRecord(_getAddress(index: _accountIndex),
index: _accountIndex, label: label);
addresses.add(address);
await save();
@ -181,9 +200,8 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
Future<void> startSync() async {
try {
syncStatus = StartingSyncStatus();
await _addressUpdateSubject?.close();
_addressUpdateSubject = eclient.addressUpdate(address: address);
await transactionHistory.update();
transactionHistory.updateAsync(onFinished: () => print('finished!'));
_subscribeForUpdates();
await _updateBalance();
syncStatus = SyncedSyncStatus();
} catch (e) {
@ -197,38 +215,101 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
Future<void> connectToNode({@required Node node}) async {
try {
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();
} catch (e) {
print(e.toString);
print(e.toString());
syncStatus = FailedSyncStatus();
}
}
@override
Future<void> createTransaction(Object credentials) async {
Future<PendingBitcoinTransaction> createTransaction(
Object credentials) async {
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 keyPair = bitcoin.ECPair.fromWIF(hd.wif);
final transactions = transactionHistory.transactions;
transactions.sort((q, w) => q.height.compareTo(w.height));
final prevTx = transactions.first;
var leftAmount = totalAmount;
final changeAddress = address;
var totalInputAmount = 0;
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();
final unspent = addresses.map((address) => eclient
.getListUnspentWithAddress(address.address)
.then((unspent) => unspent
.map((unspent) => BitcoinUnspent.fromJSON(address, unspent))));
print('Enoded transaction $encoded');
await eclient.broadcastTransaction(transactionRaw: encoded);
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;
}
}
@override
Future<void> save() async =>
await write(path: path, password: _password, data: toJSON());
if (leftAmount <= 0) {
break;
}
}
if (inputs.isEmpty) {
throw BitcoinTransactionNoInputsException();
}
if (amount <= 0 || totalInputAmount < amount) {
throw BitcoinTransactionWrongBalanceException();
}
final changeValue = totalInputAmount - amount - fee;
txb.setVersion(1);
inputs.forEach((input) {
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));
}
String toJSON() => json.encode({
'mnemonic': mnemonic,
@ -237,16 +318,32 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
'balance': balance?.toJSON()
});
String _getAddress({bitcoin.HDWallet hd, int index}) => bitcoin
.P2WPKH(
data: PaymentData(
pubkey: Uint8List.fromList(hd.derive(index).pubKey.codeUnits)))
.data
.address;
@override
double calculateEstimatedFee(TransactionPriority priority) =>
bitcoinAmountToDouble(amount: _feeMultiplier(priority));
@override
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 {
final balances = await Future.wait(
addresses.map((record) => eclient.getBalance(address: record.address)));
scriptHashes.map((sHash) => eclient.getBalance(sHash)));
final balance = balances.fold(
BitcoinBalance(confirmed: 0, unconfirmed: 0),
(BitcoinBalance acc, val) => BitcoinBalance(
@ -261,4 +358,20 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
balance = await _fetchBalances();
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:convert';
import 'dart:io';
import 'package:cake_wallet/bitcoin/script_hash.dart';
import 'package:flutter/foundation.dart';
import 'package:rxdart/rxdart.dart';
String jsonrpcparams(List<Object> params) {
final _params = params?.map((val) => '"${val.toString()}"')?.join(',');
return "[$_params]";
return '[$_params]';
}
String jsonrpc(
{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 {
SocketTask({this.completer, this.isSubscription, this.subject});
@ -50,6 +51,7 @@ class ElectrumClient {
socket.listen((List<int> event) {
try {
final jsoned = json.decode(utf8.decode(event)) as Map<String, Object>;
// print(jsoned);
final method = jsoned['method'];
if (method is String) {
@ -93,18 +95,18 @@ class ElectrumClient {
return [];
});
Future<Map<String, Object>> getBalance({String address}) =>
call(method: 'blockchain.address.get_balance', params: [address])
Future<Map<String, Object>> getBalance(String scriptHash) =>
call(method: 'blockchain.scripthash.get_balance', params: [scriptHash])
.then((dynamic result) {
if (result is Map<String, Object>) {
return result;
}
return Map<String, Object>();
return <String, Object>{};
});
Future<List<Map<String, dynamic>>> getHistory({String address}) =>
call(method: 'blockchain.address.get_history', params: [address])
Future<List<Map<String, dynamic>>> getHistory(String scriptHash) =>
call(method: 'blockchain.scripthash.get_history', params: [scriptHash])
.then((dynamic result) {
if (result is List) {
return result.map((dynamic val) {
@ -112,26 +114,94 @@ class ElectrumClient {
return val;
}
return Map<String, Object>();
return <String, Object>{};
}).toList();
}
return [];
});
Future<String> getTransactionRaw({@required String hash}) async =>
call(method: 'blockchain.transaction.get', params: [hash])
Future<List<Map<String, dynamic>>> getListUnspentWithAddress(
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) {
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 '';
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])
.then((dynamic result) {
print('result $result');
if (result is String) {
return result;
}
@ -163,11 +233,11 @@ class ElectrumClient {
return 0;
});
BehaviorSubject<Object> addressUpdate({@required String address}) =>
BehaviorSubject<Object> scripthashUpdate(String scripthash) =>
subscribe<Object>(
id: 'blockchain.address.subscribe:$address',
method: 'blockchain.address.subscribe',
params: [address]);
id: 'blockchain.scripthash.subscribe:$scripthash',
method: 'blockchain.scripthash.subscribe',
params: [scripthash]);
BehaviorSubject<T> subscribe<T>(
{@required String id,
@ -218,15 +288,12 @@ class ElectrumClient {
void _methodHandler(
{@required String method, @required Map<String, Object> request}) {
switch (method) {
case 'blockchain.address.subscribe':
case 'blockchain.scripthash.subscribe':
final params = request['params'] as List<dynamic>;
final address = params.first as String;
final id = 'blockchain.address.subscribe:$address';
if (_tasks[id] != null) {
_tasks[id].subject.add(params.last);
}
final scripthash = params.first as String;
final id = 'blockchain.scripthash.subscribe:$scripthash';
_tasks[id]?.subject?.add(params.last);
break;
default:
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) {
switch (type) {
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:
// 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:
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:cake_wallet/src/domain/common/transaction_info.dart';
@ -5,7 +6,7 @@ abstract class TransactionHistoryBase<TransactionType extends TransactionInfo> {
TransactionHistoryBase() : _isUpdating = false;
@observable
ObservableList<TransactionType> transactions;
ObservableMap<String, TransactionType> transactions;
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:cake_wallet/core/pending_transaction.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/sync_status.dart';
import 'package:cake_wallet/src/domain/common/node.dart';
@ -30,7 +32,9 @@ abstract class WalletBase<BalaceType> {
Future<void> startSync();
Future<void> createTransaction(Object credentials);
Future<PendingTransaction> createTransaction(Object credentials);
double calculateEstimatedFee(TransactionPriority priority);
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/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/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/wallet_keys_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>(
ContactService(contactSource, getIt.get<AppStore>().contactListStore));
getIt.registerSingleton<TradesStore>(TradesStore(
tradesSource: tradesSource,
settingsStore: getIt.get<SettingsStore>()));
tradesSource: tradesSource, settingsStore: getIt.get<SettingsStore>()));
getIt.registerSingleton<TradeFilterStore>(
TradeFilterStore(wallet: getIt.get<AppStore>().wallet));
getIt.registerSingleton<TransactionFilterStore>(TransactionFilterStore());
@ -143,21 +142,18 @@ Future setup(
getIt.registerFactory<WalletAddressListViewModel>(
() => WalletAddressListViewModel(wallet: getIt.get<AppStore>().wallet));
getIt.registerFactory(
() => BalanceViewModel(
getIt.registerFactory(() => BalanceViewModel(
wallet: getIt.get<AppStore>().wallet,
settingsStore: getIt.get<SettingsStore>(),
fiatConvertationStore: getIt.get<FiatConvertationStore>()));
getIt.registerFactory(
() => DashboardViewModel(
getIt.registerFactory(() => DashboardViewModel(
balanceViewModel: getIt.get<BalanceViewModel>(),
appStore: getIt.get<AppStore>(),
tradesStore: getIt.get<TradesStore>(),
tradeFilterStore: getIt.get<TradeFilterStore>(),
transactionFilterStore: getIt.get<TransactionFilterStore>(),
pageViewStore: getIt.get<PageViewStore>()
));
pageViewStore: getIt.get<PageViewStore>()));
getIt.registerFactory<AuthService>(() => AuthService(
secureStorage: getIt.get<FlutterSecureStorage>(),
@ -185,8 +181,7 @@ Future setup(
onAuthenticationFinished: onAuthFinished,
closable: false));
getIt.registerFactory<DashboardPage>(
() => DashboardPage(
getIt.registerFactory<DashboardPage>(() => DashboardPage(
walletViewModel: getIt.get<DashboardViewModel>(),
addressListViewModel: getIt.get<WalletAddressListViewModel>()));
@ -203,7 +198,9 @@ Future setup(
getIt.get<WalletAddressEditOrCreateViewModel>(param1: item)));
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(
() => SendPage(sendViewModel: getIt.get<SendViewModel>()));
@ -243,8 +240,10 @@ Future setup(
moneroAccountCreationViewModel:
getIt.get<MoneroAccountEditOrCreateViewModel>()));
getIt.registerFactory(
() => SettingsViewModel(getIt.get<AppStore>().settingsStore));
getIt.registerFactory(() {
final appStore = getIt.get<AppStore>();
return SettingsViewModel(appStore.settingsStore, appStore.wallet);
});
getIt.registerFactory(() => SettingsPage(getIt.get<SettingsViewModel>()));

View file

@ -20,12 +20,29 @@ class MoneroTransactionHistory = MoneroTransactionHistoryBase
abstract class MoneroTransactionHistoryBase
extends TransactionHistoryBase<MoneroTransactionInfo> with Store {
MoneroTransactionHistoryBase() {
transactions = ObservableList<MoneroTransactionInfo>();
transactions = ObservableMap<String, MoneroTransactionInfo>();
}
@override
Future<List<MoneroTransactionInfo>> fetchTransactions() async {
Future<Map<String, MoneroTransactionInfo>> fetchTransactions() async {
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/subaddress.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';
@ -133,7 +136,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
}
@override
Future<void> createTransaction(Object credentials) async {
Future<PendingTransaction> createTransaction(Object credentials) async {
// final _credentials = credentials as MoneroTransactionCreationCredentials;
// final transactionDescription = await transaction_history.createTransaction(
// address: _credentials.address,
@ -146,6 +149,33 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
// 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
Future<void> save() async {
// if (_isSaving) {

View file

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

View file

@ -1,40 +1,27 @@
import 'package:cake_wallet/core/address_validator.dart';
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 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:provider/provider.dart';
import 'package:cake_wallet/palette.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/primary_button.dart';
import 'package:cake_wallet/src/stores/settings/settings_store.dart';
import 'package:cake_wallet/src/stores/balance/balance_store.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/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/view_model/send/send_view_model.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/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:dotted_border/dotted_border.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/screens/send/widgets/confirm_sending_alert.dart';
import 'package:cake_wallet/src/screens/send/widgets/sending_alert.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/view_model/send/send_view_model_state.dart';
import 'package:cake_wallet/src/widgets/trail_button.dart';
// FIXME: Refactor this screen.
class SendPage extends BasePage {
SendPage({@required this.sendViewModel});
@ -53,11 +40,8 @@ class SendPage extends BasePage {
bool get resizeToAvoidBottomPadding => false;
@override
Widget trailing(context) {
// final sendStore = Provider.of<SendStore>(context);
return TrailButton(caption: S.of(context).clear, onPressed: () => null);
}
Widget trailing(context) => TrailButton(
caption: S.of(context).clear, onPressed: () => sendViewModel.reset());
@override
Widget body(BuildContext context) => SendForm(sendViewModel: sendViewModel);
@ -95,36 +79,28 @@ class SendFormState extends State<SendForm> {
}
Future<void> getOpenaliasRecord(BuildContext context) async {
final sendStore = Provider.of<SendStore>(context);
final isOpenalias =
await sendStore.isOpenaliasRecord(_addressController.text);
if (isOpenalias) {
_addressController.text = sendStore.recordAddress;
await showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).openalias_alert_title,
alertContent:
S.of(context).openalias_alert_content(sendStore.recordName),
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
}
// final sendStore = Provider.of<SendStore>(context);
// final isOpenalias =
// await sendStore.isOpenaliasRecord(_addressController.text);
//
// if (isOpenalias) {
// _addressController.text = sendStore.recordAddress;
//
// await showDialog<void>(
// context: context,
// builder: (BuildContext context) {
// return AlertWithOneAction(
// alertTitle: S.of(context).openalias_alert_title,
// alertContent:
// S.of(context).openalias_alert_content(sendStore.recordName),
// buttonText: S.of(context).ok,
// buttonAction: () => Navigator.of(context).pop());
// });
// }
}
@override
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);
return Container(
@ -140,7 +116,8 @@ class SendFormState extends State<SendForm> {
child: Column(children: <Widget>[
AddressTextField(
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,
onURIScanned: (uri) {
var address = '';
@ -163,27 +140,26 @@ class SendFormState extends State<SendForm> {
buttonColor: Theme.of(context).accentTextTheme.title.color,
validator: widget.sendViewModel.addressValidator,
),
Observer(builder: (_) {
return Padding(
Padding(
padding: const EdgeInsets.only(top: 20),
child: TextFormField(
onChanged: (value) =>
widget.sendViewModel.setCryptoAmount(value),
style: TextStyle(
fontSize: 16.0,
color: Theme.of(context)
.primaryTextTheme
.title
.color),
color:
Theme.of(context).primaryTextTheme.title.color),
controller: _cryptoAmountController,
keyboardType: TextInputType.numberWithOptions(
signed: false, decimal: true),
inputFormatters: [
BlacklistingTextInputFormatter(
RegExp('[\\-|\\ |\\,]'))
],
// inputFormatters: [
// BlacklistingTextInputFormatter(
// RegExp('[\\-|\\ |\\,]'))
// ],
decoration: InputDecoration(
prefixIcon: Padding(
padding: EdgeInsets.only(top: 12),
child: Text('XMR:',
child: Text('${widget.sendViewModel.currency.toString()}:',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
@ -195,27 +171,7 @@ class SendFormState extends State<SendForm> {
),
suffixIcon: Padding(
padding: EdgeInsets.only(bottom: 5),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment:
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(
fontSize: 16,
color: Theme.of(context)
.primaryTextTheme
.caption
.color)),
),
Container(
child: Container(
height: 32,
width: 32,
margin: EdgeInsets.only(
@ -225,11 +181,10 @@ class SendFormState extends State<SendForm> {
.accentTextTheme
.title
.color,
borderRadius: BorderRadius.all(
Radius.circular(6))),
borderRadius:
BorderRadius.all(Radius.circular(6))),
child: InkWell(
onTap: () => null,
// widget.sendViewModel,
onTap: () => widget.sendViewModel.setAll(),
child: Center(
child: Text(S.of(context).all,
textAlign: TextAlign.center,
@ -242,10 +197,7 @@ class SendFormState extends State<SendForm> {
.color)),
),
),
)
],
),
),
)),
hintStyle: TextStyle(
fontSize: 16.0,
color: Theme.of(context)
@ -262,11 +214,12 @@ class SendFormState extends State<SendForm> {
color: Theme.of(context).dividerColor,
width: 1.0))),
validator: widget.sendViewModel.amountValidator),
);
}),
),
Padding(
padding: const EdgeInsets.only(top: 20),
child: TextFormField(
onChanged: (value) =>
widget.sendViewModel.setFiatAmount(value),
style: TextStyle(
fontSize: 16.0,
color:
@ -274,10 +227,10 @@ class SendFormState extends State<SendForm> {
controller: _fiatAmountController,
keyboardType: TextInputType.numberWithOptions(
signed: false, decimal: true),
inputFormatters: [
BlacklistingTextInputFormatter(
RegExp('[\\-|\\ |\\,]'))
],
// inputFormatters: [
// BlacklistingTextInputFormatter(
// RegExp('[\\-|\\ |\\,]'))
// ],
decoration: InputDecoration(
prefixIcon: Padding(
padding: EdgeInsets.only(top: 12),
@ -426,52 +379,43 @@ class SendFormState extends State<SendForm> {
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: Observer(builder: (_) {
return LoadingPrimaryButton(
onPressed: () => null,
// syncStore.status is SyncedSyncStatus
// ? () async {
// // Hack. Don't ask me.
// FocusScope.of(context).requestFocus(FocusNode());
//
// if (_formKey.currentState.validate()) {
// await showDialog<void>(
// context: context,
// builder: (dialogContext) {
// return AlertWithTwoActions(
// alertTitle:
// S.of(context).send_creating_transaction,
// alertContent: S.of(context).confirm_sending,
// leftButtonText: S.of(context).send,
// rightButtonText: S.of(context).cancel,
// actionLeftButton: () async {
// await Navigator.of(dialogContext)
// .popAndPushNamed(Routes.auth, arguments:
// (bool isAuthenticatedSuccessfully,
// AuthPageState auth) {
// if (!isAuthenticatedSuccessfully) {
// return;
// }
//
// Navigator.of(auth.context).pop();
//
// sendStore.createTransaction(
// address: _addressController.text,
// paymentId: '');
// });
// },
// actionRightButton: () =>
// Navigator.of(context).pop());
// });
// }
// }
// : null,
onPressed: () async {
FocusScope.of(context).requestFocus(FocusNode());
if (!_formKey.currentState.validate()) {
return;
}
await showDialog<void>(
context: context,
builder: (dialogContext) {
return AlertWithTwoActions(
alertTitle: S.of(context).send_creating_transaction,
alertContent: S.of(context).confirm_sending,
leftButtonText: S.of(context).send,
rightButtonText: S.of(context).cancel,
actionLeftButton: () async {
await Navigator.of(dialogContext)
.popAndPushNamed(Routes.auth, arguments:
(bool isAuthenticatedSuccessfully,
AuthPageState auth) {
if (!isAuthenticatedSuccessfully) {
return;
}
Navigator.of(auth.context).pop();
widget.sendViewModel.createTransaction();
});
},
actionRightButton: () => Navigator.of(context).pop());
});
},
text: S.of(context).send,
color: Colors.blue,
textColor: Colors.white,
isLoading: widget.sendViewModel.state is TransactionIsCreating ||
widget.sendViewModel.state is TransactionCommitting,
isDisabled:
false // FIXME !(syncStore.status is SyncedSyncStatus),
);
isDisabled: !widget.sendViewModel.isReadyForSend);
}),
),
);
@ -482,47 +426,42 @@ class SendFormState extends State<SendForm> {
return;
}
// reaction((_) => widget.sendViewModel.fiatAmount, (String amount) {
// if (amount != _fiatAmountController.text) {
// _fiatAmountController.text = amount;
// }
// });
//
// 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);
// }
// });
reaction((_) => widget.sendViewModel.all, (bool all) {
if (all) {
_cryptoAmountController.text = S.current.all;
_fiatAmountController.text = null;
}
});
// _fiatAmountController.addListener(() {
// final fiatAmount = _fiatAmountController.text;
//
// if (sendStore.fiatAmount != fiatAmount) {
// sendStore.changeFiatAmount(fiatAmount);
// }
// });
reaction((_) => widget.sendViewModel.fiatAmount, (String amount) {
if (amount != _fiatAmountController.text) {
_fiatAmountController.text = amount;
}
});
// _cryptoAmountController.addListener(() {
// final cryptoAmount = _cryptoAmountController.text;
//
// if (sendStore.cryptoAmount != cryptoAmount) {
// sendStore.changeCryptoAmount(cryptoAmount);
// }
// });
reaction((_) => widget.sendViewModel.cryptoAmount, (String amount) {
if (widget.sendViewModel.all && amount != S.current.all) {
widget.sendViewModel.all = false;
}
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) {
if (state is SendingFailed) {
@ -540,30 +479,117 @@ class SendFormState extends State<SendForm> {
}
if (state is TransactionCreatedSuccessfully) {
// WidgetsBinding.instance.addPostFrameCallback((_) {
// showDialog<void>(
// context: context,
// builder: (BuildContext context) {
// return ConfirmSendingAlert(
// alertTitle: S.of(context).confirm_sending,
// amount: S.of(context).send_amount,
// amountValue: sendStore.pendingTransaction.amount,
// fee: S.of(context).send_fee,
// feeValue: sendStore.pendingTransaction.fee,
// leftButtonText: S.of(context).ok,
// rightButtonText: S.of(context).cancel,
// actionLeftButton: () {
// Navigator.of(context).pop();
// sendStore.commitTransaction();
// showDialog<void>(
// context: context,
// builder: (BuildContext context) {
// return SendingAlert(sendStore: sendStore);
// });
// },
// actionRightButton: () => Navigator.of(context).pop());
// });
// });
WidgetsBinding.instance.addPostFrameCallback((_) {
showDialog<void>(
context: context,
builder: (BuildContext context) {
return ConfirmSendingAlert(
alertTitle: S.of(context).confirm_sending,
amount: S.of(context).send_amount,
amountValue:
widget.sendViewModel.pendingTransaction.amountFormatted,
fee: S.of(context).send_fee,
feeValue:
widget.sendViewModel.pendingTransaction.feeFormatted,
leftButtonText: S.of(context).ok,
rightButtonText: S.of(context).cancel,
actionLeftButton: () {
Navigator.of(context).pop();
widget.sendViewModel.commitTransaction();
showDialog<void>(
context: context,
builder: (BuildContext context) {
return Observer(builder: (_) {
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) {

View file

@ -1,6 +1,6 @@
import 'dart:ui';
import 'package:cake_wallet/src/stores/send/sending_state.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/stores/send/send_store.dart';
import 'package:cake_wallet/generated/i18n.dart';

View file

@ -41,7 +41,9 @@ class SettingsPage extends BasePage {
return SettingsPickerCell<dynamic>(
title: item.title,
selectedItem: item.selectedItem(),
items: item.items);
items: item.items,
onItemSelected: (dynamic value) => item.onItemSelected(value),
);
});
}

View file

@ -4,7 +4,11 @@ import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/generated/i18n.dart';
class SettingsPickerCell<ItemType> extends StandardListRow {
SettingsPickerCell({@required String title, this.selectedItem, this.items})
SettingsPickerCell(
{@required String title,
this.selectedItem,
this.items,
this.onItemSelected})
: super(
title: title,
isSelected: false,
@ -18,11 +22,12 @@ class SettingsPickerCell<ItemType> extends StandardListRow {
selectedAtIndex: selectedAtIndex,
title: S.current.please_select,
mainAxisAlignment: MainAxisAlignment.center,
onItemSelected: (Object _) {}));
onItemSelected: (ItemType item) => onItemSelected?.call(item)));
});
final ItemType selectedItem;
final List<ItemType> items;
final void Function(ItemType item) onItemSelected;
@override
Widget buildTrailing(BuildContext context) {

View file

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

View file

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

View file

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

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

@ -5,9 +5,18 @@ class PickerListItem<ItemType> extends SettingsListItem {
PickerListItem(
{@required String title,
@required this.selectedItem,
@required this.items})
: super(title);
@required this.items,
void Function(ItemType item) onItemSelected})
: _onItemSelected = onItemSelected,
super(title);
final ItemType Function() selectedItem;
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:mobx/mobx.dart';
import 'package:cake_wallet/routes.dart';
@ -20,7 +22,8 @@ part 'settings_view_model.g.dart';
class SettingsViewModel = SettingsViewModelBase with _$SettingsViewModel;
abstract class SettingsViewModelBase with Store {
SettingsViewModelBase(this._settingsStore) : itemHeaders = {} {
SettingsViewModelBase(this._settingsStore, WalletBase wallet)
: itemHeaders = {} {
sections = [
[
PickerListItem(
@ -33,8 +36,10 @@ abstract class SettingsViewModelBase with Store {
selectedItem: () => fiatCurrency),
PickerListItem(
title: S.current.settings_fee_priority,
items: TransactionPriority.all,
selectedItem: () => transactionPriority),
items: _transactionPriorities(wallet.type),
selectedItem: () => transactionPriority,
onItemSelected: (TransactionPriority priority) =>
_settingsStore.transactionPriority = priority),
SwitcherListItem(
title: S.current.settings_save_recipient_address,
value: () => shouldSaveRecipientAddress,
@ -146,12 +151,9 @@ abstract class SettingsViewModelBase with Store {
_settingsStore.allowBiometricalAuthentication = value;
// @observable
// bool isDarkTheme;
//
// @observable
// int defaultPinLength;
// @observable
final Map<String, String> itemHeaders;
List<List<SettingsListItem>> sections;
final SettingsStore _settingsStore;
@ -182,4 +184,24 @@ abstract class SettingsViewModelBase with Store {
@action
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 [];
}
}
}