mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-12-22 11:39:22 +00:00
TMP
This commit is contained in:
parent
4572049c5d
commit
5eefd6a31b
35 changed files with 1273 additions and 618 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -89,4 +89,6 @@ android/key.properties
|
|||
**/tool/.secrets-prod.json
|
||||
**/lib/.secrets.g.dart
|
||||
|
||||
vendor/
|
||||
vendor/
|
||||
|
||||
android/app/.cxx/**
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
final content = await read(path: path, password: _password);
|
||||
return json.decode(content) as Map<String, Object>;
|
||||
}
|
||||
|
||||
Future<void> _load() 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);
|
||||
}
|
||||
final content = await _read();
|
||||
final txs = content['transactions'] as Map<String, Object> ?? {};
|
||||
|
||||
return null;
|
||||
})
|
||||
.where((el) => el != null)
|
||||
.toList();
|
||||
txs.entries.forEach((entry) {
|
||||
final val = entry.value;
|
||||
|
||||
return {'transactions': transactions, 'height': height};
|
||||
} catch (_) {
|
||||
return {'transactions': <BitcoinTransactionInfo>[], 'height': 0};
|
||||
if (val is Map<String, Object>) {
|
||||
final tx = BitcoinTransactionInfo.fromJson(val);
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,34 +24,87 @@ 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;
|
||||
|
||||
tx.outs.forEach((out) {
|
||||
try {
|
||||
final p2pkh = bitcoin.P2PKH(
|
||||
data: PaymentData(output: out.script), network: bitcoin.bitcoin);
|
||||
exist = addresses.contains(p2pkh.data.address);
|
||||
if (addresses != null) {
|
||||
tx.outs.forEach((out) {
|
||||
try {
|
||||
final p2pkh = bitcoin.P2PKH(
|
||||
data: PaymentData(output: out.script), network: bitcoin.bitcoin);
|
||||
exist = addresses.contains(p2pkh.data.address);
|
||||
|
||||
if (exist) {
|
||||
amount += out.value;
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
if (exist) {
|
||||
amount += out.value;
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
4
lib/bitcoin/bitcoin_transaction_no_inputs_exception.dart
Normal file
4
lib/bitcoin/bitcoin_transaction_no_inputs_exception.dart
Normal file
|
@ -0,0 +1,4 @@
|
|||
class BitcoinTransactionNoInputsException implements Exception {
|
||||
@override
|
||||
String toString() => 'No inputs for the transaction.';
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
class BitcoinTransactionWrongBalanceException implements Exception {
|
||||
@override
|
||||
String toString() => 'Wrong balance. Not enough BTC on your balance.';
|
||||
}
|
17
lib/bitcoin/bitcoin_unspent.dart
Normal file
17
lib/bitcoin/bitcoin_unspent.dart
Normal 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');
|
||||
}
|
|
@ -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,39 +215,102 @@ 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;
|
||||
|
||||
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.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');
|
||||
await eclient.broadcastTransaction(transactionRaw: encoded);
|
||||
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));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save() async =>
|
||||
await write(path: path, password: _password, data: toJSON());
|
||||
|
||||
String toJSON() => json.encode({
|
||||
'mnemonic': mnemonic,
|
||||
'account_index': _accountIndex.toString(),
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
47
lib/bitcoin/pending_bitcoin_transaction.dart
Normal file
47
lib/bitcoin/pending_bitcoin_transaction.dart
Normal 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);
|
||||
}
|
18
lib/bitcoin/script_hash.dart
Normal file
18
lib/bitcoin/script_hash.dart
Normal 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
26
lib/bitcoin/utils.dart
Normal 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;
|
|
@ -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 '';
|
||||
}
|
||||
|
|
6
lib/core/pending_transaction.dart
Normal file
6
lib/core/pending_transaction.dart
Normal file
|
@ -0,0 +1,6 @@
|
|||
mixin PendingTransaction {
|
||||
String get amountFormatted;
|
||||
String get feeFormatted;
|
||||
|
||||
Future<void> commit();
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
47
lib/di.dart
47
lib/di.dart
|
@ -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(
|
||||
wallet: getIt.get<AppStore>().wallet,
|
||||
settingsStore: getIt.get<SettingsStore>(),
|
||||
fiatConvertationStore: getIt.get<FiatConvertationStore>()));
|
||||
getIt.registerFactory(() => BalanceViewModel(
|
||||
wallet: getIt.get<AppStore>().wallet,
|
||||
settingsStore: getIt.get<SettingsStore>(),
|
||||
fiatConvertationStore: getIt.get<FiatConvertationStore>()));
|
||||
|
||||
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>()
|
||||
));
|
||||
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>()));
|
||||
|
||||
getIt.registerFactory<AuthService>(() => AuthService(
|
||||
secureStorage: getIt.get<FlutterSecureStorage>(),
|
||||
|
@ -185,10 +181,9 @@ Future setup(
|
|||
onAuthenticationFinished: onAuthFinished,
|
||||
closable: false));
|
||||
|
||||
getIt.registerFactory<DashboardPage>(
|
||||
() => DashboardPage(
|
||||
walletViewModel: getIt.get<DashboardViewModel>(),
|
||||
addressListViewModel: getIt.get<WalletAddressListViewModel>()));
|
||||
getIt.registerFactory<DashboardPage>(() => DashboardPage(
|
||||
walletViewModel: getIt.get<DashboardViewModel>(),
|
||||
addressListViewModel: getIt.get<WalletAddressListViewModel>()));
|
||||
|
||||
getIt.registerFactory<ReceivePage>(() => ReceivePage(
|
||||
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>()));
|
||||
|
||||
|
|
|
@ -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}) {}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,110 +140,86 @@ class SendFormState extends State<SendForm> {
|
|||
buttonColor: Theme.of(context).accentTextTheme.title.color,
|
||||
validator: widget.sendViewModel.addressValidator,
|
||||
),
|
||||
Observer(builder: (_) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: TextFormField(
|
||||
style: TextStyle(
|
||||
fontSize: 16.0,
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.title
|
||||
.color),
|
||||
controller: _cryptoAmountController,
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
signed: false, decimal: true),
|
||||
inputFormatters: [
|
||||
BlacklistingTextInputFormatter(
|
||||
RegExp('[\\-|\\ |\\,]'))
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Padding(
|
||||
padding: EdgeInsets.only(top: 12),
|
||||
child: Text('XMR:',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
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),
|
||||
controller: _cryptoAmountController,
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
signed: false, decimal: true),
|
||||
// inputFormatters: [
|
||||
// BlacklistingTextInputFormatter(
|
||||
// RegExp('[\\-|\\ |\\,]'))
|
||||
// ],
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Padding(
|
||||
padding: EdgeInsets.only(top: 12),
|
||||
child: Text('${widget.sendViewModel.currency.toString()}:',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
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)
|
||||
.primaryTextTheme
|
||||
.accentTextTheme
|
||||
.title
|
||||
.color,
|
||||
)),
|
||||
),
|
||||
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,
|
||||
borderRadius:
|
||||
BorderRadius.all(Radius.circular(6))),
|
||||
child: InkWell(
|
||||
onTap: () => widget.sendViewModel.setAll(),
|
||||
child: Center(
|
||||
child: Text(S.of(context).all,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.caption
|
||||
.color)),
|
||||
),
|
||||
Container(
|
||||
height: 32,
|
||||
width: 32,
|
||||
margin: EdgeInsets.only(
|
||||
left: 12, bottom: 7, top: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.accentTextTheme
|
||||
.title
|
||||
.color,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(6))),
|
||||
child: InkWell(
|
||||
onTap: () => null,
|
||||
// widget.sendViewModel,
|
||||
child: Center(
|
||||
child: Text(S.of(context).all,
|
||||
textAlign: TextAlign.center,
|
||||
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),
|
||||
);
|
||||
}),
|
||||
),
|
||||
)),
|
||||
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: 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) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -39,9 +39,11 @@ class SettingsPage extends BasePage {
|
|||
if (item is PickerListItem) {
|
||||
return Observer(builder: (_) {
|
||||
return SettingsPickerCell<dynamic>(
|
||||
title: item.title,
|
||||
selectedItem: item.selectedItem(),
|
||||
items: item.items);
|
||||
title: item.title,
|
||||
selectedItem: item.selectedItem(),
|
||||
items: item.items,
|
||||
onItemSelected: (dynamic value) => item.onItemSelected(value),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -4,25 +4,30 @@ 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,
|
||||
onTap: (BuildContext context) async {
|
||||
final selectedAtIndex = items.indexOf(selectedItem);
|
||||
title: title,
|
||||
isSelected: false,
|
||||
onTap: (BuildContext context) async {
|
||||
final selectedAtIndex = items.indexOf(selectedItem);
|
||||
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => Picker(
|
||||
items: items,
|
||||
selectedAtIndex: selectedAtIndex,
|
||||
title: S.current.please_select,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
onItemSelected: (Object _) {}));
|
||||
});
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => Picker(
|
||||
items: items,
|
||||
selectedAtIndex: selectedAtIndex,
|
||||
title: S.current.please_select,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
onItemSelected: (ItemType item) => onItemSelected?.call(item)));
|
||||
});
|
||||
|
||||
final ItemType selectedItem;
|
||||
final List<ItemType> items;
|
||||
final void Function(ItemType item) onItemSelected;
|
||||
|
||||
@override
|
||||
Widget buildTrailing(BuildContext context) {
|
||||
|
@ -35,4 +40,4 @@ class SettingsPickerCell<ItemType> extends StandardListRow {
|
|||
color: Theme.of(context).primaryTextTheme.caption.color),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) ??
|
||||
|
|
|
@ -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}) {
|
||||
|
|
|
@ -29,24 +29,24 @@ part 'dashboard_view_model.g.dart';
|
|||
class DashboardViewModel = DashboardViewModelBase with _$DashboardViewModel;
|
||||
|
||||
abstract class DashboardViewModelBase with Store {
|
||||
DashboardViewModelBase({
|
||||
this.balanceViewModel,
|
||||
this.appStore,
|
||||
this.tradesStore,
|
||||
this.tradeFilterStore,
|
||||
this.transactionFilterStore,
|
||||
this.pageViewStore}) {
|
||||
|
||||
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,
|
||||
fiatCurrency: appStore.settingsStore.fiatCurrency,
|
||||
displayMode: balanceDisplayMode)));
|
||||
transaction: transaction,
|
||||
price: price,
|
||||
fiatCurrency: appStore.settingsStore.fiatCurrency,
|
||||
displayMode: balanceDisplayMode)));
|
||||
|
||||
_reaction = reaction((_) => appStore.wallet, _onWalletChange);
|
||||
|
||||
|
@ -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,11 +132,11 @@ abstract class DashboardViewModelBase with Store {
|
|||
void _onWalletChange(WalletBase wallet) {
|
||||
name = wallet.name;
|
||||
transactions.clear();
|
||||
transactions.addAll(wallet.transactionHistory.transactions
|
||||
.map((transaction) => TransactionListItem(
|
||||
transaction: transaction,
|
||||
price: price,
|
||||
fiatCurrency: appStore.settingsStore.fiatCurrency,
|
||||
displayMode: balanceDisplayMode)));
|
||||
transactions.addAll(wallet.transactionHistory.transactions.values.map(
|
||||
(transaction) => TransactionListItem(
|
||||
transaction: transaction,
|
||||
price: price,
|
||||
fiatCurrency: appStore.settingsStore.fiatCurrency,
|
||||
displayMode: balanceDisplayMode)));
|
||||
}
|
||||
}
|
||||
|
|
171
lib/view_model/send/send_view_model.dart
Normal file
171
lib/view_model/send/send_view_model.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
18
lib/view_model/send/send_view_model_state.dart
Normal file
18
lib/view_model/send/send_view_model_state.dart
Normal 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;
|
||||
}
|
|
@ -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 {}
|
||||
}
|
|
@ -4,10 +4,19 @@ import 'package:cake_wallet/view_model/settings/settings_list_item.dart';
|
|||
class PickerListItem<ItemType> extends SettingsListItem {
|
||||
PickerListItem(
|
||||
{@required String title,
|
||||
@required this.selectedItem,
|
||||
@required this.items})
|
||||
: super(title);
|
||||
@required this.selectedItem,
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue