mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-01-08 20:09:24 +00:00
Estimated fee rates.
This commit is contained in:
parent
b1047fb3cf
commit
35aabcd248
12 changed files with 252 additions and 168 deletions
|
@ -357,7 +357,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 25;
|
CURRENT_PROJECT_VERSION = 26;
|
||||||
DEVELOPMENT_TEAM = 32J6BB6VUS;
|
DEVELOPMENT_TEAM = 32J6BB6VUS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
|
@ -374,7 +374,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Flutter",
|
"$(PROJECT_DIR)/Flutter",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 4.1.1;
|
MARKETING_VERSION = 4.1.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet;
|
PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
@ -498,7 +498,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 25;
|
CURRENT_PROJECT_VERSION = 26;
|
||||||
DEVELOPMENT_TEAM = 32J6BB6VUS;
|
DEVELOPMENT_TEAM = 32J6BB6VUS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
|
@ -515,7 +515,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Flutter",
|
"$(PROJECT_DIR)/Flutter",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 4.1.1;
|
MARKETING_VERSION = 4.1.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet;
|
PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
@ -533,7 +533,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CURRENT_PROJECT_VERSION = 25;
|
CURRENT_PROJECT_VERSION = 26;
|
||||||
DEVELOPMENT_TEAM = 32J6BB6VUS;
|
DEVELOPMENT_TEAM = 32J6BB6VUS;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
|
@ -550,7 +550,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"$(PROJECT_DIR)/Flutter",
|
"$(PROJECT_DIR)/Flutter",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 4.1.1;
|
MARKETING_VERSION = 4.1.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet;
|
PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
|
|
@ -2,32 +2,30 @@ import 'package:cake_wallet/entities/transaction_priority.dart';
|
||||||
import 'package:cake_wallet/generated/i18n.dart';
|
import 'package:cake_wallet/generated/i18n.dart';
|
||||||
|
|
||||||
class BitcoinTransactionPriority extends TransactionPriority {
|
class BitcoinTransactionPriority extends TransactionPriority {
|
||||||
const BitcoinTransactionPriority(this.rate, {String title, int raw})
|
const BitcoinTransactionPriority({String title, int raw})
|
||||||
: super(title: title, raw: raw);
|
: super(title: title, raw: raw);
|
||||||
|
|
||||||
static const List<BitcoinTransactionPriority> all = [slow, medium, fast];
|
static const List<BitcoinTransactionPriority> all = [fast, medium, slow];
|
||||||
static const BitcoinTransactionPriority slow =
|
static const BitcoinTransactionPriority slow =
|
||||||
BitcoinTransactionPriority(11, title: 'Slow', raw: 0);
|
BitcoinTransactionPriority(title: 'Slow', raw: 0);
|
||||||
static const BitcoinTransactionPriority medium =
|
static const BitcoinTransactionPriority medium =
|
||||||
BitcoinTransactionPriority(90, title: 'Medium', raw: 1);
|
BitcoinTransactionPriority(title: 'Medium', raw: 1);
|
||||||
static const BitcoinTransactionPriority fast =
|
static const BitcoinTransactionPriority fast =
|
||||||
BitcoinTransactionPriority(98, title: 'Fast', raw: 2);
|
BitcoinTransactionPriority(title: 'Fast', raw: 2);
|
||||||
|
|
||||||
static BitcoinTransactionPriority deserialize({int raw}) {
|
static BitcoinTransactionPriority deserialize({int raw}) {
|
||||||
switch (raw) {
|
switch (raw) {
|
||||||
case 0:
|
case 0:
|
||||||
return slow;
|
return slow;
|
||||||
case 2:
|
case 1:
|
||||||
return medium;
|
return medium;
|
||||||
case 3:
|
case 2:
|
||||||
return fast;
|
return fast;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final int rate;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
var label = '';
|
var label = '';
|
||||||
|
@ -46,6 +44,6 @@ class BitcoinTransactionPriority extends TransactionPriority {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return '$label ($rate sat/byte)';
|
return label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
|
||||||
syncStatus = NotConnectedSyncStatus(),
|
syncStatus = NotConnectedSyncStatus(),
|
||||||
_password = password,
|
_password = password,
|
||||||
_accountIndex = accountIndex,
|
_accountIndex = accountIndex,
|
||||||
|
_feeRates = <int>[],
|
||||||
super(walletInfo) {
|
super(walletInfo) {
|
||||||
_unspent = [];
|
_unspent = [];
|
||||||
_scripthashesUpdateSubject = {};
|
_scripthashesUpdateSubject = {};
|
||||||
|
@ -118,10 +119,6 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
|
||||||
walletInfo: walletInfo);
|
walletInfo: walletInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int feeAmountForPriority(BitcoinTransactionPriority priority,
|
|
||||||
int inputsCount, int outputsCount) =>
|
|
||||||
priority.rate * estimatedTransactionSize(inputsCount, outputsCount);
|
|
||||||
|
|
||||||
static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
|
static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
|
||||||
inputsCount * 146 + outputsCounts * 33 + 8;
|
inputsCount * 146 + outputsCounts * 33 + 8;
|
||||||
|
|
||||||
|
@ -161,6 +158,7 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
|
||||||
wif: hd.wif, privateKey: hd.privKey, publicKey: hd.pubKey);
|
wif: hd.wif, privateKey: hd.privKey, publicKey: hd.pubKey);
|
||||||
|
|
||||||
final String _password;
|
final String _password;
|
||||||
|
List<int> _feeRates;
|
||||||
int _accountIndex;
|
int _accountIndex;
|
||||||
Map<String, BehaviorSubject<Object>> _scripthashesUpdateSubject;
|
Map<String, BehaviorSubject<Object>> _scripthashesUpdateSubject;
|
||||||
|
|
||||||
|
@ -233,6 +231,11 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
|
||||||
_subscribeForUpdates();
|
_subscribeForUpdates();
|
||||||
await _updateBalance();
|
await _updateBalance();
|
||||||
await _updateUnspent();
|
await _updateUnspent();
|
||||||
|
_feeRates = await eclient.feeRates();
|
||||||
|
|
||||||
|
Timer.periodic(const Duration(minutes: 1),
|
||||||
|
(timer) async => _feeRates = await eclient.feeRates());
|
||||||
|
|
||||||
syncStatus = SyncedSyncStatus();
|
syncStatus = SyncedSyncStatus();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print(e.toString());
|
print(e.toString());
|
||||||
|
@ -332,7 +335,7 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
|
||||||
addressToOutputScript(transactionCredentials.address), amount);
|
addressToOutputScript(transactionCredentials.address), amount);
|
||||||
|
|
||||||
final estimatedSize = estimatedTransactionSize(inputs.length, 2);
|
final estimatedSize = estimatedTransactionSize(inputs.length, 2);
|
||||||
final feeAmount = transactionCredentials.priority.rate * estimatedSize;
|
final feeAmount = feeRate(transactionCredentials.priority) * estimatedSize;
|
||||||
final changeValue = totalInputAmount - amount - feeAmount;
|
final changeValue = totalInputAmount - amount - feeAmount;
|
||||||
|
|
||||||
if (changeValue > minAmount) {
|
if (changeValue > minAmount) {
|
||||||
|
@ -362,6 +365,18 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
|
||||||
'balance': balance?.toJSON()
|
'balance': balance?.toJSON()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
int feeRate(TransactionPriority priority) {
|
||||||
|
if (priority is BitcoinTransactionPriority) {
|
||||||
|
return _feeRates[priority.raw];
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount,
|
||||||
|
int outputsCount) =>
|
||||||
|
feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int calculateEstimatedFee(TransactionPriority priority, int amount) {
|
int calculateEstimatedFee(TransactionPriority priority, int amount) {
|
||||||
if (priority is BitcoinTransactionPriority) {
|
if (priority is BitcoinTransactionPriority) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
|
||||||
import 'package:cake_wallet/bitcoin/script_hash.dart';
|
import 'package:cake_wallet/bitcoin/script_hash.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
@ -22,9 +23,8 @@ String jsonrpcparams(List<Object> params) {
|
||||||
}
|
}
|
||||||
|
|
||||||
String jsonrpc(
|
String jsonrpc(
|
||||||
{String method, List<Object> params, int id, double version = 2.0}) =>
|
{String method, List<Object> params, int id, double version = 2.0}) =>
|
||||||
'{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json
|
'{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json.encode(params)}}\n';
|
||||||
.encode(params)}}\n';
|
|
||||||
|
|
||||||
class SocketTask {
|
class SocketTask {
|
||||||
SocketTask({this.completer, this.isSubscription, this.subject});
|
SocketTask({this.completer, this.isSubscription, this.subject});
|
||||||
|
@ -77,7 +77,7 @@ class ElectrumClient {
|
||||||
socket.listen((Uint8List event) {
|
socket.listen((Uint8List event) {
|
||||||
try {
|
try {
|
||||||
final response =
|
final response =
|
||||||
json.decode(utf8.decode(event.toList())) as Map<String, Object>;
|
json.decode(utf8.decode(event.toList())) as Map<String, Object>;
|
||||||
_handleResponse(response);
|
_handleResponse(response);
|
||||||
} on FormatException catch (e) {
|
} on FormatException catch (e) {
|
||||||
final msg = e.message.toLowerCase();
|
final msg = e.message.toLowerCase();
|
||||||
|
@ -93,7 +93,7 @@ class ElectrumClient {
|
||||||
|
|
||||||
if (isJSONStringCorrect(unterminatedString)) {
|
if (isJSONStringCorrect(unterminatedString)) {
|
||||||
final response =
|
final response =
|
||||||
json.decode(unterminatedString) as Map<String, Object>;
|
json.decode(unterminatedString) as Map<String, Object>;
|
||||||
_handleResponse(response);
|
_handleResponse(response);
|
||||||
unterminatedString = '';
|
unterminatedString = '';
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@ class ElectrumClient {
|
||||||
|
|
||||||
if (isJSONStringCorrect(unterminatedString)) {
|
if (isJSONStringCorrect(unterminatedString)) {
|
||||||
final response =
|
final response =
|
||||||
json.decode(unterminatedString) as Map<String, Object>;
|
json.decode(unterminatedString) as Map<String, Object>;
|
||||||
_handleResponse(response);
|
_handleResponse(response);
|
||||||
unterminatedString = null;
|
unterminatedString = null;
|
||||||
}
|
}
|
||||||
|
@ -173,7 +173,7 @@ class ElectrumClient {
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<List<Map<String, dynamic>>> getListUnspentWithAddress(
|
Future<List<Map<String, dynamic>>> getListUnspentWithAddress(
|
||||||
String address) =>
|
String address) =>
|
||||||
call(
|
call(
|
||||||
method: 'blockchain.scripthash.listunspent',
|
method: 'blockchain.scripthash.listunspent',
|
||||||
params: [scriptHash(address)]).then((dynamic result) {
|
params: [scriptHash(address)]).then((dynamic result) {
|
||||||
|
@ -253,7 +253,7 @@ class ElectrumClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> broadcastTransaction(
|
Future<String> broadcastTransaction(
|
||||||
{@required String transactionRaw}) async =>
|
{@required String transactionRaw}) async =>
|
||||||
call(method: 'blockchain.transaction.broadcast', params: [transactionRaw])
|
call(method: 'blockchain.transaction.broadcast', params: [transactionRaw])
|
||||||
.then((dynamic result) {
|
.then((dynamic result) {
|
||||||
if (result is String) {
|
if (result is String) {
|
||||||
|
@ -264,14 +264,14 @@ class ElectrumClient {
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getMerkle(
|
Future<Map<String, dynamic>> getMerkle(
|
||||||
{@required String hash, @required int height}) async =>
|
{@required String hash, @required int height}) async =>
|
||||||
await call(
|
await call(
|
||||||
method: 'blockchain.transaction.get_merkle',
|
method: 'blockchain.transaction.get_merkle',
|
||||||
params: [hash, height]) as Map<String, dynamic>;
|
params: [hash, height]) as Map<String, dynamic>;
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getHeader({@required int height}) async =>
|
Future<Map<String, dynamic>> getHeader({@required int height}) async =>
|
||||||
await call(method: 'blockchain.block.get_header', params: [height])
|
await call(method: 'blockchain.block.get_header', params: [height])
|
||||||
as Map<String, dynamic>;
|
as Map<String, dynamic>;
|
||||||
|
|
||||||
Future<double> estimatefee({@required int p}) =>
|
Future<double> estimatefee({@required int p}) =>
|
||||||
call(method: 'blockchain.estimatefee', params: [p])
|
call(method: 'blockchain.estimatefee', params: [p])
|
||||||
|
@ -287,6 +287,32 @@ class ElectrumClient {
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Future<List<List<int>>> feeHistogram() =>
|
||||||
|
call(method: 'mempool.get_fee_histogram').then((dynamic result) {
|
||||||
|
if (result is List) {
|
||||||
|
return result.map((dynamic e) {
|
||||||
|
if (e is List) {
|
||||||
|
return e.map((dynamic ee) => ee is int ? ee : null).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<List<int>> feeRates() async {
|
||||||
|
final topDoubleString = await estimatefee(p: 1);
|
||||||
|
final middleDoubleString = await estimatefee(p: 20);
|
||||||
|
final bottomDoubleString = await estimatefee(p: 150);
|
||||||
|
final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round();
|
||||||
|
final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round();
|
||||||
|
final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round();
|
||||||
|
|
||||||
|
return [bottom, middle, top];
|
||||||
|
}
|
||||||
|
|
||||||
BehaviorSubject<Object> scripthashUpdate(String scripthash) {
|
BehaviorSubject<Object> scripthashUpdate(String scripthash) {
|
||||||
_id += 1;
|
_id += 1;
|
||||||
return subscribe<Object>(
|
return subscribe<Object>(
|
||||||
|
@ -295,9 +321,10 @@ class ElectrumClient {
|
||||||
params: [scripthash]);
|
params: [scripthash]);
|
||||||
}
|
}
|
||||||
|
|
||||||
BehaviorSubject<T> subscribe<T>({@required String id,
|
BehaviorSubject<T> subscribe<T>(
|
||||||
@required String method,
|
{@required String id,
|
||||||
List<Object> params = const []}) {
|
@required String method,
|
||||||
|
List<Object> params = const []}) {
|
||||||
final subscription = BehaviorSubject<T>();
|
final subscription = BehaviorSubject<T>();
|
||||||
_regisrySubscription(id, subscription);
|
_regisrySubscription(id, subscription);
|
||||||
socket.write(jsonrpc(method: method, id: _id, params: params));
|
socket.write(jsonrpc(method: method, id: _id, params: params));
|
||||||
|
@ -315,9 +342,10 @@ class ElectrumClient {
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> callWithTimeout({String method,
|
Future<dynamic> callWithTimeout(
|
||||||
List<Object> params = const [],
|
{String method,
|
||||||
int timeout = 2000}) async {
|
List<Object> params = const [],
|
||||||
|
int timeout = 2000}) async {
|
||||||
final completer = Completer<dynamic>();
|
final completer = Completer<dynamic>();
|
||||||
_id += 1;
|
_id += 1;
|
||||||
final id = _id;
|
final id = _id;
|
||||||
|
@ -329,7 +357,6 @@ class ElectrumClient {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -339,9 +366,8 @@ class ElectrumClient {
|
||||||
onConnectionStatusChange = null;
|
onConnectionStatusChange = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _registryTask(int id, Completer completer) =>
|
void _registryTask(int id, Completer completer) => _tasks[id.toString()] =
|
||||||
_tasks[id.toString()] =
|
SocketTask(completer: completer, isSubscription: false);
|
||||||
SocketTask(completer: completer, isSubscription: false);
|
|
||||||
|
|
||||||
void _regisrySubscription(String id, BehaviorSubject subject) =>
|
void _regisrySubscription(String id, BehaviorSubject subject) =>
|
||||||
_tasks[id] = SocketTask(subject: subject, isSubscription: true);
|
_tasks[id] = SocketTask(subject: subject, isSubscription: true);
|
||||||
|
|
|
@ -761,6 +761,7 @@ class SendPage extends BasePage {
|
||||||
await showPopUp<void>(
|
await showPopUp<void>(
|
||||||
builder: (_) => Picker(
|
builder: (_) => Picker(
|
||||||
items: items,
|
items: items,
|
||||||
|
displayItem: sendViewModel.displayFeeRate,
|
||||||
selectedAtIndex: selectedItem,
|
selectedAtIndex: selectedItem,
|
||||||
title: S.of(context).please_select,
|
title: S.of(context).please_select,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
|
|
@ -41,6 +41,7 @@ class SettingsPage extends BasePage {
|
||||||
if (item is PickerListItem) {
|
if (item is PickerListItem) {
|
||||||
return Observer(builder: (_) {
|
return Observer(builder: (_) {
|
||||||
return SettingsPickerCell<dynamic>(
|
return SettingsPickerCell<dynamic>(
|
||||||
|
displayItem: item.displayItem,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
selectedItem: item.selectedItem(),
|
selectedItem: item.selectedItem(),
|
||||||
items: item.items,
|
items: item.items,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import 'package:cake_wallet/generated/i18n.dart';
|
||||||
class SettingsPickerCell<ItemType> extends StandardListRow {
|
class SettingsPickerCell<ItemType> extends StandardListRow {
|
||||||
SettingsPickerCell(
|
SettingsPickerCell(
|
||||||
{@required String title,
|
{@required String title,
|
||||||
|
@required this.displayItem,
|
||||||
this.selectedItem,
|
this.selectedItem,
|
||||||
this.items,
|
this.items,
|
||||||
this.onItemSelected})
|
this.onItemSelected})
|
||||||
|
@ -20,6 +21,7 @@ class SettingsPickerCell<ItemType> extends StandardListRow {
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => Picker(
|
builder: (_) => Picker(
|
||||||
items: items,
|
items: items,
|
||||||
|
displayItem: displayItem,
|
||||||
selectedAtIndex: selectedAtIndex,
|
selectedAtIndex: selectedAtIndex,
|
||||||
title: S.current.please_select,
|
title: S.current.please_select,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
@ -30,11 +32,12 @@ class SettingsPickerCell<ItemType> extends StandardListRow {
|
||||||
final ItemType selectedItem;
|
final ItemType selectedItem;
|
||||||
final List<ItemType> items;
|
final List<ItemType> items;
|
||||||
final void Function(ItemType item) onItemSelected;
|
final void Function(ItemType item) onItemSelected;
|
||||||
|
final String Function(ItemType item) displayItem;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildTrailing(BuildContext context) {
|
Widget buildTrailing(BuildContext context) {
|
||||||
return Text(
|
return Text(
|
||||||
selectedItem.toString(),
|
displayItem?.call(selectedItem) ?? selectedItem.toString(),
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14.0,
|
fontSize: 14.0,
|
||||||
|
|
|
@ -10,10 +10,11 @@ class Picker<Item extends Object> extends StatefulWidget {
|
||||||
Picker({
|
Picker({
|
||||||
@required this.selectedAtIndex,
|
@required this.selectedAtIndex,
|
||||||
@required this.items,
|
@required this.items,
|
||||||
this.images,
|
|
||||||
@required this.title,
|
@required this.title,
|
||||||
this.description,
|
|
||||||
@required this.onItemSelected,
|
@required this.onItemSelected,
|
||||||
|
this.displayItem,
|
||||||
|
this.images,
|
||||||
|
this.description,
|
||||||
this.mainAxisAlignment = MainAxisAlignment.start,
|
this.mainAxisAlignment = MainAxisAlignment.start,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -24,6 +25,7 @@ class Picker<Item extends Object> extends StatefulWidget {
|
||||||
final String description;
|
final String description;
|
||||||
final Function(Item) onItemSelected;
|
final Function(Item) onItemSelected;
|
||||||
final MainAxisAlignment mainAxisAlignment;
|
final MainAxisAlignment mainAxisAlignment;
|
||||||
|
final String Function(Item) displayItem;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
PickerState createState() => PickerState<Item>(items, images, onItemSelected);
|
PickerState createState() => PickerState<Item>(items, images, onItemSelected);
|
||||||
|
@ -36,7 +38,8 @@ class PickerState<Item> extends State<Picker> {
|
||||||
final List<Item> items;
|
final List<Item> items;
|
||||||
final List<Image> images;
|
final List<Image> images;
|
||||||
|
|
||||||
final closeButton = Image.asset('assets/images/close.png',
|
final closeButton = Image.asset(
|
||||||
|
'assets/images/close.png',
|
||||||
color: Palette.darkBlueCraiola,
|
color: Palette.darkBlueCraiola,
|
||||||
);
|
);
|
||||||
ScrollController controller = ScrollController();
|
ScrollController controller = ScrollController();
|
||||||
|
@ -49,7 +52,9 @@ class PickerState<Item> extends State<Picker> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
controller.addListener(() {
|
controller.addListener(() {
|
||||||
fromTop = controller.hasClients
|
fromTop = controller.hasClients
|
||||||
? (controller.offset / controller.position.maxScrollExtent * (backgroundHeight - thumbHeight))
|
? (controller.offset /
|
||||||
|
controller.position.maxScrollExtent *
|
||||||
|
(backgroundHeight - thumbHeight))
|
||||||
: 0;
|
: 0;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
});
|
||||||
|
@ -58,134 +63,142 @@ class PickerState<Item> extends State<Picker> {
|
||||||
|
|
||||||
return AlertBackground(
|
return AlertBackground(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Column(
|
Container(
|
||||||
mainAxisSize: MainAxisSize.min,
|
padding: EdgeInsets.only(left: 24, right: 24),
|
||||||
children: <Widget>[
|
child: Text(
|
||||||
Container(
|
widget.title,
|
||||||
padding: EdgeInsets.only(left: 24, right: 24),
|
textAlign: TextAlign.center,
|
||||||
child: Text(
|
style: TextStyle(
|
||||||
widget.title,
|
fontSize: 18,
|
||||||
textAlign: TextAlign.center,
|
fontFamily: 'Lato',
|
||||||
style: TextStyle(
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 18,
|
decoration: TextDecoration.none,
|
||||||
fontFamily: 'Lato',
|
color: Colors.white),
|
||||||
fontWeight: FontWeight.bold,
|
),
|
||||||
decoration: TextDecoration.none,
|
),
|
||||||
color: Colors.white
|
Padding(
|
||||||
),
|
padding: EdgeInsets.only(left: 24, right: 24, top: 24),
|
||||||
),
|
child: GestureDetector(
|
||||||
),
|
onTap: () => null,
|
||||||
Padding(
|
child: ClipRRect(
|
||||||
padding: EdgeInsets.only(left: 24, right: 24, top: 24),
|
borderRadius: BorderRadius.all(Radius.circular(14)),
|
||||||
child: GestureDetector(
|
child: Container(
|
||||||
onTap: () => null,
|
height: 233,
|
||||||
child: ClipRRect(
|
color: Theme.of(context).accentTextTheme.title.color,
|
||||||
borderRadius: BorderRadius.all(Radius.circular(14)),
|
child: Stack(
|
||||||
child: Container(
|
alignment: Alignment.center,
|
||||||
height: 233,
|
children: <Widget>[
|
||||||
color: Theme.of(context).accentTextTheme.title.color,
|
ListView.separated(
|
||||||
child: Stack(
|
padding: EdgeInsets.all(0),
|
||||||
alignment: Alignment.center,
|
controller: controller,
|
||||||
children: <Widget>[
|
separatorBuilder: (context, index) => Divider(
|
||||||
ListView.separated(
|
color: Theme.of(context)
|
||||||
padding: EdgeInsets.all(0),
|
.accentTextTheme
|
||||||
controller: controller,
|
.title
|
||||||
separatorBuilder: (context, index) => Divider(
|
.backgroundColor,
|
||||||
color: Theme.of(context).accentTextTheme.title.backgroundColor,
|
height: 1,
|
||||||
height: 1,
|
),
|
||||||
),
|
itemCount: items == null ? 0 : items.length,
|
||||||
itemCount: items == null ? 0 : items.length,
|
itemBuilder: (context, index) {
|
||||||
itemBuilder: (context, index) {
|
final item = items[index];
|
||||||
final item = items[index];
|
final image =
|
||||||
final image = images != null? images[index] : null;
|
images != null ? images[index] : null;
|
||||||
final isItemSelected = index == widget.selectedAtIndex;
|
final isItemSelected =
|
||||||
|
index == widget.selectedAtIndex;
|
||||||
|
|
||||||
final color = isItemSelected
|
final color = isItemSelected
|
||||||
? Theme.of(context).textTheme.body2.color
|
? Theme.of(context).textTheme.body2.color
|
||||||
: Theme.of(context).accentTextTheme.title.color;
|
: Theme.of(context)
|
||||||
final textColor = isItemSelected
|
.accentTextTheme
|
||||||
? Palette.blueCraiola
|
.title
|
||||||
: Theme.of(context).primaryTextTheme.title.color;
|
.color;
|
||||||
|
final textColor = isItemSelected
|
||||||
|
? Palette.blueCraiola
|
||||||
|
: Theme.of(context)
|
||||||
|
.primaryTextTheme
|
||||||
|
.title
|
||||||
|
.color;
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (onItemSelected == null) {
|
if (onItemSelected == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
onItemSelected(item);
|
onItemSelected(item);
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
height: 77,
|
|
||||||
padding: EdgeInsets.only(left: 24, right: 24),
|
|
||||||
color: color,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
mainAxisAlignment: widget.mainAxisAlignment,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
image ?? Offstage(),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
left: image != null ? 12 : 0
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
item.toString(),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontFamily: 'Lato',
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: textColor,
|
|
||||||
decoration: TextDecoration.none,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
child: Container(
|
||||||
((widget.description != null)
|
height: 77,
|
||||||
&&(widget.description.isNotEmpty))
|
padding: EdgeInsets.only(left: 24, right: 24),
|
||||||
? Positioned(
|
color: color,
|
||||||
bottom: 24,
|
child: Row(
|
||||||
left: 24,
|
mainAxisSize: MainAxisSize.max,
|
||||||
right: 24,
|
mainAxisAlignment: widget.mainAxisAlignment,
|
||||||
child: Text(
|
crossAxisAlignment:
|
||||||
widget.description,
|
CrossAxisAlignment.center,
|
||||||
textAlign: TextAlign.center,
|
children: <Widget>[
|
||||||
style: TextStyle(
|
image ?? Offstage(),
|
||||||
fontSize: 12,
|
Padding(
|
||||||
fontWeight: FontWeight.w500,
|
padding: EdgeInsets.only(
|
||||||
fontFamily: 'Lato',
|
left: image != null ? 12 : 0),
|
||||||
decoration: TextDecoration.none,
|
child: Text(
|
||||||
color: Theme.of(context).primaryTextTheme
|
widget.displayItem?.call(item) ??
|
||||||
.title.color
|
item.toString(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontFamily: 'Lato',
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: textColor,
|
||||||
|
decoration: TextDecoration.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
)
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
((widget.description != null) &&
|
||||||
|
(widget.description.isNotEmpty))
|
||||||
|
? Positioned(
|
||||||
|
bottom: 24,
|
||||||
|
left: 24,
|
||||||
|
right: 24,
|
||||||
|
child: Text(
|
||||||
|
widget.description,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontFamily: 'Lato',
|
||||||
|
decoration: TextDecoration.none,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.primaryTextTheme
|
||||||
|
.title
|
||||||
|
.color),
|
||||||
|
))
|
||||||
: Offstage(),
|
: Offstage(),
|
||||||
isShowScrollThumb
|
isShowScrollThumb
|
||||||
? CakeScrollbar(
|
? CakeScrollbar(
|
||||||
backgroundHeight: backgroundHeight,
|
backgroundHeight: backgroundHeight,
|
||||||
thumbHeight: thumbHeight,
|
thumbHeight: thumbHeight,
|
||||||
fromTop: fromTop
|
fromTop: fromTop)
|
||||||
)
|
|
||||||
: Offstage(),
|
: Offstage(),
|
||||||
],
|
],
|
||||||
)
|
)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
AlertCloseButton(image: closeButton)
|
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
);
|
AlertCloseButton(image: closeButton)
|
||||||
|
],
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -97,7 +97,8 @@ abstract class SendViewModelBase with Store {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final fee = _wallet.calculateEstimatedFee(_settingsStore.priority[_wallet.type], amount);
|
final fee = _wallet.calculateEstimatedFee(
|
||||||
|
_settingsStore.priority[_wallet.type], amount);
|
||||||
|
|
||||||
if (_wallet is BitcoinWallet) {
|
if (_wallet is BitcoinWallet) {
|
||||||
return bitcoinAmountToDouble(amount: fee);
|
return bitcoinAmountToDouble(amount: fee);
|
||||||
|
@ -298,7 +299,8 @@ abstract class SendViewModelBase with Store {
|
||||||
final amount = !sendAll ? _amount : null;
|
final amount = !sendAll ? _amount : null;
|
||||||
final priority = _settingsStore.priority[_wallet.type];
|
final priority = _settingsStore.priority[_wallet.type];
|
||||||
|
|
||||||
return BitcoinTransactionCredentials(address, amount, priority as BitcoinTransactionPriority);
|
return BitcoinTransactionCredentials(
|
||||||
|
address, amount, priority as BitcoinTransactionPriority);
|
||||||
case WalletType.monero:
|
case WalletType.monero:
|
||||||
final amount = !sendAll ? _amount : null;
|
final amount = !sendAll ? _amount : null;
|
||||||
final priority = _settingsStore.priority[_wallet.type];
|
final priority = _settingsStore.priority[_wallet.type];
|
||||||
|
@ -345,4 +347,16 @@ abstract class SendViewModelBase with Store {
|
||||||
|
|
||||||
void removeTemplate({Template template}) =>
|
void removeTemplate({Template template}) =>
|
||||||
_sendTemplateStore.remove(template: template);
|
_sendTemplateStore.remove(template: template);
|
||||||
|
|
||||||
|
String displayFeeRate(dynamic priority) {
|
||||||
|
final _priority = priority as TransactionPriority;
|
||||||
|
final wallet = _wallet;
|
||||||
|
|
||||||
|
if (wallet is BitcoinWallet) {
|
||||||
|
final rate = wallet.feeRate(_priority);
|
||||||
|
return '${priority.toString()} ($rate sat/byte)';
|
||||||
|
}
|
||||||
|
|
||||||
|
return priority.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,14 @@ class PickerListItem<ItemType> extends SettingsListItem {
|
||||||
{@required String title,
|
{@required String title,
|
||||||
@required this.selectedItem,
|
@required this.selectedItem,
|
||||||
@required this.items,
|
@required this.items,
|
||||||
|
this.displayItem,
|
||||||
void Function(ItemType item) onItemSelected})
|
void Function(ItemType item) onItemSelected})
|
||||||
: _onItemSelected = onItemSelected,
|
: _onItemSelected = onItemSelected,
|
||||||
super(title);
|
super(title);
|
||||||
|
|
||||||
final ItemType Function() selectedItem;
|
final ItemType Function() selectedItem;
|
||||||
final List<ItemType> items;
|
final List<ItemType> items;
|
||||||
|
final String Function(ItemType item) displayItem;
|
||||||
final void Function(ItemType item) _onItemSelected;
|
final void Function(ItemType item) _onItemSelected;
|
||||||
|
|
||||||
void onItemSelected(dynamic item) {
|
void onItemSelected(dynamic item) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart';
|
import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart';
|
||||||
|
import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
|
||||||
import 'package:cake_wallet/entities/balance.dart';
|
import 'package:cake_wallet/entities/balance.dart';
|
||||||
import 'package:cake_wallet/entities/transaction_priority.dart';
|
import 'package:cake_wallet/entities/transaction_priority.dart';
|
||||||
import 'package:cake_wallet/themes/theme_base.dart';
|
import 'package:cake_wallet/themes/theme_base.dart';
|
||||||
|
@ -74,6 +75,16 @@ abstract class SettingsViewModelBase with Store {
|
||||||
PickerListItem(
|
PickerListItem(
|
||||||
title: S.current.settings_fee_priority,
|
title: S.current.settings_fee_priority,
|
||||||
items: priorityForWalletType(wallet.type),
|
items: priorityForWalletType(wallet.type),
|
||||||
|
displayItem: (dynamic priority) {
|
||||||
|
final _priority = priority as TransactionPriority;
|
||||||
|
|
||||||
|
if (wallet is BitcoinWallet) {
|
||||||
|
final rate = wallet.feeRate(_priority);
|
||||||
|
return '${priority.toString()} ($rate sat/byte)';
|
||||||
|
}
|
||||||
|
|
||||||
|
return priority.toString();
|
||||||
|
},
|
||||||
selectedItem: () => transactionPriority,
|
selectedItem: () => transactionPriority,
|
||||||
onItemSelected: (TransactionPriority priority) =>
|
onItemSelected: (TransactionPriority priority) =>
|
||||||
_settingsStore.priority[wallet.type] = priority),
|
_settingsStore.priority[wallet.type] = priority),
|
||||||
|
|
|
@ -11,7 +11,7 @@ description: Cake Wallet.
|
||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 4.1.1+40
|
version: 4.1.2+41
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.7.0 <3.0.0"
|
sdk: ">=2.7.0 <3.0.0"
|
||||||
|
|
Loading…
Reference in a new issue