Estimated fee rates.

This commit is contained in:
M 2021-02-13 00:38:34 +02:00
parent b1047fb3cf
commit 35aabcd248
12 changed files with 252 additions and 168 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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