Estimated fee rates.

M committed Feb 13, 2021 at 00:38 UTC 35aabcd24839fbdb5d5fc533dc51fb60c09f7f1e
12 files changed +252 -168
ios/Runner.xcodeproj/project.pbxproj
+6 -6
@@ -357,7 +357,7 @@
357 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
358 CLANG_ENABLE_MODULES = YES;
359 CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
360 - CURRENT_PROJECT_VERSION = 25;
360 + CURRENT_PROJECT_VERSION = 26;
361 DEVELOPMENT_TEAM = 32J6BB6VUS;
362 ENABLE_BITCODE = NO;
363 FRAMEWORK_SEARCH_PATHS = (
@@ -374,7 +374,7 @@
374 "$(inherited)",
375 "$(PROJECT_DIR)/Flutter",
376 );
377 - MARKETING_VERSION = 4.1.1;
377 + MARKETING_VERSION = 4.1.2;
378 PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet;
379 PRODUCT_NAME = "$(TARGET_NAME)";
380 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -498,7 +498,7 @@
498 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
499 CLANG_ENABLE_MODULES = YES;
500 CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
501 - CURRENT_PROJECT_VERSION = 25;
501 + CURRENT_PROJECT_VERSION = 26;
502 DEVELOPMENT_TEAM = 32J6BB6VUS;
503 ENABLE_BITCODE = NO;
504 FRAMEWORK_SEARCH_PATHS = (
@@ -515,7 +515,7 @@
515 "$(inherited)",
516 "$(PROJECT_DIR)/Flutter",
517 );
518 - MARKETING_VERSION = 4.1.1;
518 + MARKETING_VERSION = 4.1.2;
519 PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet;
520 PRODUCT_NAME = "$(TARGET_NAME)";
521 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -533,7 +533,7 @@
533 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
534 CLANG_ENABLE_MODULES = YES;
535 CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
536 - CURRENT_PROJECT_VERSION = 25;
536 + CURRENT_PROJECT_VERSION = 26;
537 DEVELOPMENT_TEAM = 32J6BB6VUS;
538 ENABLE_BITCODE = NO;
539 FRAMEWORK_SEARCH_PATHS = (
@@ -550,7 +550,7 @@
550 "$(inherited)",
551 "$(PROJECT_DIR)/Flutter",
552 );
553 - MARKETING_VERSION = 4.1.1;
553 + MARKETING_VERSION = 4.1.2;
554 PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet;
555 PRODUCT_NAME = "$(TARGET_NAME)";
556 SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
lib/bitcoin/bitcoin_transaction_priority.dart
+8 -10
@@ -2,32 +2,30 @@ import 'package:cake_wallet/entities/transaction_priority.dart';
2 import 'package:cake_wallet/generated/i18n.dart';
3
4 class BitcoinTransactionPriority extends TransactionPriority {
5 - const BitcoinTransactionPriority(this.rate, {String title, int raw})
5 + const BitcoinTransactionPriority({String title, int raw})
6 : super(title: title, raw: raw);
7
8 - static const List<BitcoinTransactionPriority> all = [slow, medium, fast];
8 + static const List<BitcoinTransactionPriority> all = [fast, medium, slow];
9 static const BitcoinTransactionPriority slow =
10 - BitcoinTransactionPriority(11, title: 'Slow', raw: 0);
10 + BitcoinTransactionPriority(title: 'Slow', raw: 0);
11 static const BitcoinTransactionPriority medium =
12 - BitcoinTransactionPriority(90, title: 'Medium', raw: 1);
12 + BitcoinTransactionPriority(title: 'Medium', raw: 1);
13 static const BitcoinTransactionPriority fast =
14 - BitcoinTransactionPriority(98, title: 'Fast', raw: 2);
14 + BitcoinTransactionPriority(title: 'Fast', raw: 2);
15
16 static BitcoinTransactionPriority deserialize({int raw}) {
17 switch (raw) {
18 case 0:
19 return slow;
20 - case 2:
20 + case 1:
21 return medium;
22 - case 3:
22 + case 2:
23 return fast;
24 default:
25 return null;
26 }
27 }
28
29 - final int rate;
30 -
29 @override
30 String toString() {
31 var label = '';
@@ -46,6 +44,6 @@ class BitcoinTransactionPriority extends TransactionPriority {
44 break;
45 }
46
49 - return '$label ($rate sat/byte)';
47 + return label;
48 }
49 }
lib/bitcoin/bitcoin_wallet.dart
+20 -5
@@ -53,6 +53,7 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
53 syncStatus = NotConnectedSyncStatus(),
54 _password = password,
55 _accountIndex = accountIndex,
56 + _feeRates = <int>[],
57 super(walletInfo) {
58 _unspent = [];
59 _scripthashesUpdateSubject = {};
@@ -118,10 +119,6 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
119 walletInfo: walletInfo);
120 }
121
121 - static int feeAmountForPriority(BitcoinTransactionPriority priority,
122 - int inputsCount, int outputsCount) =>
123 - priority.rate * estimatedTransactionSize(inputsCount, outputsCount);
124 -
122 static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
123 inputsCount * 146 + outputsCounts * 33 + 8;
124
@@ -161,6 +158,7 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
158 wif: hd.wif, privateKey: hd.privKey, publicKey: hd.pubKey);
159
160 final String _password;
161 + List<int> _feeRates;
162 int _accountIndex;
163 Map<String, BehaviorSubject<Object>> _scripthashesUpdateSubject;
164
@@ -233,6 +231,11 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
231 _subscribeForUpdates();
232 await _updateBalance();
233 await _updateUnspent();
234 + _feeRates = await eclient.feeRates();
235 +
236 + Timer.periodic(const Duration(minutes: 1),
237 + (timer) async => _feeRates = await eclient.feeRates());
238 +
239 syncStatus = SyncedSyncStatus();
240 } catch (e) {
241 print(e.toString());
@@ -332,7 +335,7 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
335 addressToOutputScript(transactionCredentials.address), amount);
336
337 final estimatedSize = estimatedTransactionSize(inputs.length, 2);
335 - final feeAmount = transactionCredentials.priority.rate * estimatedSize;
338 + final feeAmount = feeRate(transactionCredentials.priority) * estimatedSize;
339 final changeValue = totalInputAmount - amount - feeAmount;
340
341 if (changeValue > minAmount) {
@@ -362,6 +365,18 @@ abstract class BitcoinWalletBase extends WalletBase<BitcoinBalance> with Store {
365 'balance': balance?.toJSON()
366 });
367
368 + int feeRate(TransactionPriority priority) {
369 + if (priority is BitcoinTransactionPriority) {
370 + return _feeRates[priority.raw];
371 + }
372 +
373 + return 0;
374 + }
375 +
376 + int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount,
377 + int outputsCount) =>
378 + feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount);
379 +
380 @override
381 int calculateEstimatedFee(TransactionPriority priority, int amount) {
382 if (priority is BitcoinTransactionPriority) {
lib/bitcoin/electrum.dart
+46 -20
@@ -2,6 +2,7 @@ import 'dart:async';
2 import 'dart:convert';
3 import 'dart:io';
4 import 'dart:typed_data';
5 +import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
6 import 'package:cake_wallet/bitcoin/script_hash.dart';
7 import 'package:flutter/foundation.dart';
8 import 'package:rxdart/rxdart.dart';
@@ -22,9 +23,8 @@ String jsonrpcparams(List<Object> params) {
23 }
24
25 String jsonrpc(
25 - {String method, List<Object> params, int id, double version = 2.0}) =>
26 - '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json
27 - .encode(params)}}\n';
26 + {String method, List<Object> params, int id, double version = 2.0}) =>
27 + '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json.encode(params)}}\n';
28
29 class SocketTask {
30 SocketTask({this.completer, this.isSubscription, this.subject});
@@ -77,7 +77,7 @@ class ElectrumClient {
77 socket.listen((Uint8List event) {
78 try {
79 final response =
80 - json.decode(utf8.decode(event.toList())) as Map<String, Object>;
80 + json.decode(utf8.decode(event.toList())) as Map<String, Object>;
81 _handleResponse(response);
82 } on FormatException catch (e) {
83 final msg = e.message.toLowerCase();
@@ -93,7 +93,7 @@ class ElectrumClient {
93
94 if (isJSONStringCorrect(unterminatedString)) {
95 final response =
96 - json.decode(unterminatedString) as Map<String, Object>;
96 + json.decode(unterminatedString) as Map<String, Object>;
97 _handleResponse(response);
98 unterminatedString = '';
99 }
@@ -107,7 +107,7 @@ class ElectrumClient {
107
108 if (isJSONStringCorrect(unterminatedString)) {
109 final response =
110 - json.decode(unterminatedString) as Map<String, Object>;
110 + json.decode(unterminatedString) as Map<String, Object>;
111 _handleResponse(response);
112 unterminatedString = null;
113 }
@@ -173,7 +173,7 @@ class ElectrumClient {
173 });
174
175 Future<List<Map<String, dynamic>>> getListUnspentWithAddress(
176 - String address) =>
176 + String address) =>
177 call(
178 method: 'blockchain.scripthash.listunspent',
179 params: [scriptHash(address)]).then((dynamic result) {
@@ -253,7 +253,7 @@ class ElectrumClient {
253 }
254
255 Future<String> broadcastTransaction(
256 - {@required String transactionRaw}) async =>
256 + {@required String transactionRaw}) async =>
257 call(method: 'blockchain.transaction.broadcast', params: [transactionRaw])
258 .then((dynamic result) {
259 if (result is String) {
@@ -264,14 +264,14 @@ class ElectrumClient {
264 });
265
266 Future<Map<String, dynamic>> getMerkle(
267 - {@required String hash, @required int height}) async =>
267 + {@required String hash, @required int height}) async =>
268 await call(
269 method: 'blockchain.transaction.get_merkle',
270 params: [hash, height]) as Map<String, dynamic>;
271
272 Future<Map<String, dynamic>> getHeader({@required int height}) async =>
273 await call(method: 'blockchain.block.get_header', params: [height])
274 - as Map<String, dynamic>;
274 + as Map<String, dynamic>;
275
276 Future<double> estimatefee({@required int p}) =>
277 call(method: 'blockchain.estimatefee', params: [p])
@@ -287,6 +287,32 @@ class ElectrumClient {
287 return 0;
288 });
289
290 + Future<List<List<int>>> feeHistogram() =>
291 + call(method: 'mempool.get_fee_histogram').then((dynamic result) {
292 + if (result is List) {
293 + return result.map((dynamic e) {
294 + if (e is List) {
295 + return e.map((dynamic ee) => ee is int ? ee : null).toList();
296 + }
297 +
298 + return null;
299 + }).toList();
300 + }
301 +
302 + return [];
303 + });
304 +
305 + Future<List<int>> feeRates() async {
306 + final topDoubleString = await estimatefee(p: 1);
307 + final middleDoubleString = await estimatefee(p: 20);
308 + final bottomDoubleString = await estimatefee(p: 150);
309 + final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round();
310 + final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round();
311 + final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round();
312 +
313 + return [bottom, middle, top];
314 + }
315 +
316 BehaviorSubject<Object> scripthashUpdate(String scripthash) {
317 _id += 1;
318 return subscribe<Object>(
@@ -295,9 +321,10 @@ class ElectrumClient {
321 params: [scripthash]);
322 }
323
298 - BehaviorSubject<T> subscribe<T>({@required String id,
299 - @required String method,
300 - List<Object> params = const []}) {
324 + BehaviorSubject<T> subscribe<T>(
325 + {@required String id,
326 + @required String method,
327 + List<Object> params = const []}) {
328 final subscription = BehaviorSubject<T>();
329 _regisrySubscription(id, subscription);
330 socket.write(jsonrpc(method: method, id: _id, params: params));
@@ -315,9 +342,10 @@ class ElectrumClient {
342 return completer.future;
343 }
344
318 - Future<dynamic> callWithTimeout({String method,
319 - List<Object> params = const [],
320 - int timeout = 2000}) async {
345 + Future<dynamic> callWithTimeout(
346 + {String method,
347 + List<Object> params = const [],
348 + int timeout = 2000}) async {
349 final completer = Completer<dynamic>();
350 _id += 1;
351 final id = _id;
@@ -329,7 +357,6 @@ class ElectrumClient {
357 }
358 });
359
332 -
360 return completer.future;
361 }
362
@@ -339,9 +366,8 @@ class ElectrumClient {
366 onConnectionStatusChange = null;
367 }
368
342 - void _registryTask(int id, Completer completer) =>
343 - _tasks[id.toString()] =
344 - SocketTask(completer: completer, isSubscription: false);
369 + void _registryTask(int id, Completer completer) => _tasks[id.toString()] =
370 + SocketTask(completer: completer, isSubscription: false);
371
372 void _regisrySubscription(String id, BehaviorSubject subject) =>
373 _tasks[id] = SocketTask(subject: subject, isSubscription: true);
lib/src/screens/send/send_page.dart
+1
@@ -761,6 +761,7 @@ class SendPage extends BasePage {
761 await showPopUp<void>(
762 builder: (_) => Picker(
763 items: items,
764 + displayItem: sendViewModel.displayFeeRate,
765 selectedAtIndex: selectedItem,
766 title: S.of(context).please_select,
767 mainAxisAlignment: MainAxisAlignment.center,
lib/src/screens/settings/settings.dart
+1
@@ -41,6 +41,7 @@ class SettingsPage extends BasePage {
41 if (item is PickerListItem) {
42 return Observer(builder: (_) {
43 return SettingsPickerCell<dynamic>(
44 + displayItem: item.displayItem,
45 title: item.title,
46 selectedItem: item.selectedItem(),
47 items: item.items,
lib/src/screens/settings/widgets/settings_picker_cell.dart
+4 -1
@@ -7,6 +7,7 @@ import 'package:cake_wallet/generated/i18n.dart';
7 class SettingsPickerCell<ItemType> extends StandardListRow {
8 SettingsPickerCell(
9 {@required String title,
10 + @required this.displayItem,
11 this.selectedItem,
12 this.items,
13 this.onItemSelected})
@@ -20,6 +21,7 @@ class SettingsPickerCell<ItemType> extends StandardListRow {
21 context: context,
22 builder: (_) => Picker(
23 items: items,
24 + displayItem: displayItem,
25 selectedAtIndex: selectedAtIndex,
26 title: S.current.please_select,
27 mainAxisAlignment: MainAxisAlignment.center,
@@ -30,11 +32,12 @@ class SettingsPickerCell<ItemType> extends StandardListRow {
32 final ItemType selectedItem;
33 final List<ItemType> items;
34 final void Function(ItemType item) onItemSelected;
35 + final String Function(ItemType item) displayItem;
36
37 @override
38 Widget buildTrailing(BuildContext context) {
39 return Text(
37 - selectedItem.toString(),
40 + displayItem?.call(selectedItem) ?? selectedItem.toString(),
41 textAlign: TextAlign.right,
42 style: TextStyle(
43 fontSize: 14.0,
lib/src/widgets/picker.dart
+136 -123
@@ -10,10 +10,11 @@ class Picker<Item extends Object> extends StatefulWidget {
10 Picker({
11 @required this.selectedAtIndex,
12 @required this.items,
13 - this.images,
13 @required this.title,
15 - this.description,
14 @required this.onItemSelected,
15 + this.displayItem,
16 + this.images,
17 + this.description,
18 this.mainAxisAlignment = MainAxisAlignment.start,
19 });
20
@@ -24,6 +25,7 @@ class Picker<Item extends Object> extends StatefulWidget {
25 final String description;
26 final Function(Item) onItemSelected;
27 final MainAxisAlignment mainAxisAlignment;
28 + final String Function(Item) displayItem;
29
30 @override
31 PickerState createState() => PickerState<Item>(items, images, onItemSelected);
@@ -36,7 +38,8 @@ class PickerState<Item> extends State<Picker> {
38 final List<Item> items;
39 final List<Image> images;
40
39 - final closeButton = Image.asset('assets/images/close.png',
41 + final closeButton = Image.asset(
42 + 'assets/images/close.png',
43 color: Palette.darkBlueCraiola,
44 );
45 ScrollController controller = ScrollController();
@@ -49,7 +52,9 @@ class PickerState<Item> extends State<Picker> {
52 Widget build(BuildContext context) {
53 controller.addListener(() {
54 fromTop = controller.hasClients
52 - ? (controller.offset / controller.position.maxScrollExtent * (backgroundHeight - thumbHeight))
55 + ? (controller.offset /
56 + controller.position.maxScrollExtent *
57 + (backgroundHeight - thumbHeight))
58 : 0;
59 setState(() {});
60 });
@@ -58,134 +63,142 @@ class PickerState<Item> extends State<Picker> {
63
64 return AlertBackground(
65 child: Stack(
61 - alignment: Alignment.center,
66 + alignment: Alignment.center,
67 + children: <Widget>[
68 + Column(
69 + mainAxisSize: MainAxisSize.min,
70 children: <Widget>[
63 - Column(
64 - mainAxisSize: MainAxisSize.min,
65 - children: <Widget>[
66 - Container(
67 - padding: EdgeInsets.only(left: 24, right: 24),
68 - child: Text(
69 - widget.title,
70 - textAlign: TextAlign.center,
71 - style: TextStyle(
72 - fontSize: 18,
73 - fontFamily: 'Lato',
74 - fontWeight: FontWeight.bold,
75 - decoration: TextDecoration.none,
76 - color: Colors.white
77 - ),
78 - ),
79 - ),
80 - Padding(
81 - padding: EdgeInsets.only(left: 24, right: 24, top: 24),
82 - child: GestureDetector(
83 - onTap: () => null,
84 - child: ClipRRect(
85 - borderRadius: BorderRadius.all(Radius.circular(14)),
86 - child: Container(
87 - height: 233,
88 - color: Theme.of(context).accentTextTheme.title.color,
89 - child: Stack(
90 - alignment: Alignment.center,
91 - children: <Widget>[
92 - ListView.separated(
93 - padding: EdgeInsets.all(0),
94 - controller: controller,
95 - separatorBuilder: (context, index) => Divider(
96 - color: Theme.of(context).accentTextTheme.title.backgroundColor,
97 - height: 1,
98 - ),
99 - itemCount: items == null ? 0 : items.length,
100 - itemBuilder: (context, index) {
101 - final item = items[index];
102 - final image = images != null? images[index] : null;
103 - final isItemSelected = index == widget.selectedAtIndex;
71 + Container(
72 + padding: EdgeInsets.only(left: 24, right: 24),
73 + child: Text(
74 + widget.title,
75 + textAlign: TextAlign.center,
76 + style: TextStyle(
77 + fontSize: 18,
78 + fontFamily: 'Lato',
79 + fontWeight: FontWeight.bold,
80 + decoration: TextDecoration.none,
81 + color: Colors.white),
82 + ),
83 + ),
84 + Padding(
85 + padding: EdgeInsets.only(left: 24, right: 24, top: 24),
86 + child: GestureDetector(
87 + onTap: () => null,
88 + child: ClipRRect(
89 + borderRadius: BorderRadius.all(Radius.circular(14)),
90 + child: Container(
91 + height: 233,
92 + color: Theme.of(context).accentTextTheme.title.color,
93 + child: Stack(
94 + alignment: Alignment.center,
95 + children: <Widget>[
96 + ListView.separated(
97 + padding: EdgeInsets.all(0),
98 + controller: controller,
99 + separatorBuilder: (context, index) => Divider(
100 + color: Theme.of(context)
101 + .accentTextTheme
102 + .title
103 + .backgroundColor,
104 + height: 1,
105 + ),
106 + itemCount: items == null ? 0 : items.length,
107 + itemBuilder: (context, index) {
108 + final item = items[index];
109 + final image =
110 + images != null ? images[index] : null;
111 + final isItemSelected =
112 + index == widget.selectedAtIndex;
113
105 - final color = isItemSelected
106 - ? Theme.of(context).textTheme.body2.color
107 - : Theme.of(context).accentTextTheme.title.color;
108 - final textColor = isItemSelected
109 - ? Palette.blueCraiola
110 - : Theme.of(context).primaryTextTheme.title.color;
114 + final color = isItemSelected
115 + ? Theme.of(context).textTheme.body2.color
116 + : Theme.of(context)
117 + .accentTextTheme
118 + .title
119 + .color;
120 + final textColor = isItemSelected
121 + ? Palette.blueCraiola
122 + : Theme.of(context)
123 + .primaryTextTheme
124 + .title
125 + .color;
126
112 - return GestureDetector(
113 - onTap: () {
114 - if (onItemSelected == null) {
115 - return;
116 - }
117 - Navigator.of(context).pop();
118 - onItemSelected(item);
119 - },
120 - child: Container(
121 - height: 77,
122 - padding: EdgeInsets.only(left: 24, right: 24),
123 - color: color,
124 - child: Row(
125 - mainAxisSize: MainAxisSize.max,
126 - mainAxisAlignment: widget.mainAxisAlignment,
127 - crossAxisAlignment: CrossAxisAlignment.center,
128 - children: <Widget>[
129 - image ?? Offstage(),
130 - Padding(
131 - padding: EdgeInsets.only(
132 - left: image != null ? 12 : 0
133 - ),
134 - child: Text(
135 - item.toString(),
136 - style: TextStyle(
137 - fontSize: 18,
138 - fontFamily: 'Lato',
139 - fontWeight: FontWeight.w600,
140 - color: textColor,
141 - decoration: TextDecoration.none,
142 - ),
143 - ),
144 - )
145 - ],
146 - ),
147 - ),
148 - );
127 + return GestureDetector(
128 + onTap: () {
129 + if (onItemSelected == null) {
130 + return;
131 + }
132 + Navigator.of(context).pop();
133 + onItemSelected(item);
134 },
150 - ),
151 - ((widget.description != null)
152 - &&(widget.description.isNotEmpty))
153 - ? Positioned(
154 - bottom: 24,
155 - left: 24,
156 - right: 24,
157 - child: Text(
158 - widget.description,
159 - textAlign: TextAlign.center,
160 - style: TextStyle(
161 - fontSize: 12,
162 - fontWeight: FontWeight.w500,
163 - fontFamily: 'Lato',
164 - decoration: TextDecoration.none,
165 - color: Theme.of(context).primaryTextTheme
166 - .title.color
135 + child: Container(
136 + height: 77,
137 + padding: EdgeInsets.only(left: 24, right: 24),
138 + color: color,
139 + child: Row(
140 + mainAxisSize: MainAxisSize.max,
141 + mainAxisAlignment: widget.mainAxisAlignment,
142 + crossAxisAlignment:
143 + CrossAxisAlignment.center,
144 + children: <Widget>[
145 + image ?? Offstage(),
146 + Padding(
147 + padding: EdgeInsets.only(
148 + left: image != null ? 12 : 0),
149 + child: Text(
150 + widget.displayItem?.call(item) ??
151 + item.toString(),
152 + style: TextStyle(
153 + fontSize: 18,
154 + fontFamily: 'Lato',
155 + fontWeight: FontWeight.w600,
156 + color: textColor,
157 + decoration: TextDecoration.none,
158 + ),
159 + ),
160 + )
161 + ],
162 ),
168 - )
169 - )
163 + ),
164 + );
165 + },
166 + ),
167 + ((widget.description != null) &&
168 + (widget.description.isNotEmpty))
169 + ? Positioned(
170 + bottom: 24,
171 + left: 24,
172 + right: 24,
173 + child: Text(
174 + widget.description,
175 + textAlign: TextAlign.center,
176 + style: TextStyle(
177 + fontSize: 12,
178 + fontWeight: FontWeight.w500,
179 + fontFamily: 'Lato',
180 + decoration: TextDecoration.none,
181 + color: Theme.of(context)
182 + .primaryTextTheme
183 + .title
184 + .color),
185 + ))
186 : Offstage(),
171 - isShowScrollThumb
187 + isShowScrollThumb
188 ? CakeScrollbar(
189 backgroundHeight: backgroundHeight,
190 thumbHeight: thumbHeight,
175 - fromTop: fromTop
176 - )
191 + fromTop: fromTop)
192 : Offstage(),
178 - ],
179 - )
180 - ),
181 - ),
182 - ),
183 - )
184 - ],
185 - ),
186 - AlertCloseButton(image: closeButton)
193 + ],
194 + )),
195 + ),
196 + ),
197 + )
198 ],
188 - )
189 - );
199 + ),
200 + AlertCloseButton(image: closeButton)
201 + ],
202 + ));
203 }
191 -}
\ No newline at end of file
204 +}
lib/view_model/send/send_view_model.dart
+16 -2
@@ -97,7 +97,8 @@ abstract class SendViewModelBase with Store {
97 }
98 }
99
100 - final fee = _wallet.calculateEstimatedFee(_settingsStore.priority[_wallet.type], amount);
100 + final fee = _wallet.calculateEstimatedFee(
101 + _settingsStore.priority[_wallet.type], amount);
102
103 if (_wallet is BitcoinWallet) {
104 return bitcoinAmountToDouble(amount: fee);
@@ -298,7 +299,8 @@ abstract class SendViewModelBase with Store {
299 final amount = !sendAll ? _amount : null;
300 final priority = _settingsStore.priority[_wallet.type];
301
301 - return BitcoinTransactionCredentials(address, amount, priority as BitcoinTransactionPriority);
302 + return BitcoinTransactionCredentials(
303 + address, amount, priority as BitcoinTransactionPriority);
304 case WalletType.monero:
305 final amount = !sendAll ? _amount : null;
306 final priority = _settingsStore.priority[_wallet.type];
@@ -345,4 +347,16 @@ abstract class SendViewModelBase with Store {
347
348 void removeTemplate({Template template}) =>
349 _sendTemplateStore.remove(template: template);
350 +
351 + String displayFeeRate(dynamic priority) {
352 + final _priority = priority as TransactionPriority;
353 + final wallet = _wallet;
354 +
355 + if (wallet is BitcoinWallet) {
356 + final rate = wallet.feeRate(_priority);
357 + return '${priority.toString()} ($rate sat/byte)';
358 + }
359 +
360 + return priority.toString();
361 + }
362 }
lib/view_model/settings/picker_list_item.dart
+2
@@ -6,12 +6,14 @@ class PickerListItem<ItemType> extends SettingsListItem {
6 {@required String title,
7 @required this.selectedItem,
8 @required this.items,
9 + this.displayItem,
10 void Function(ItemType item) onItemSelected})
11 : _onItemSelected = onItemSelected,
12 super(title);
13
14 final ItemType Function() selectedItem;
15 final List<ItemType> items;
16 + final String Function(ItemType item) displayItem;
17 final void Function(ItemType item) _onItemSelected;
18
19 void onItemSelected(dynamic item) {
lib/view_model/settings/settings_view_model.dart
+11
@@ -1,4 +1,5 @@
1 import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart';
2 +import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart';
3 import 'package:cake_wallet/entities/balance.dart';
4 import 'package:cake_wallet/entities/transaction_priority.dart';
5 import 'package:cake_wallet/themes/theme_base.dart';
@@ -74,6 +75,16 @@ abstract class SettingsViewModelBase with Store {
75 PickerListItem(
76 title: S.current.settings_fee_priority,
77 items: priorityForWalletType(wallet.type),
78 + displayItem: (dynamic priority) {
79 + final _priority = priority as TransactionPriority;
80 +
81 + if (wallet is BitcoinWallet) {
82 + final rate = wallet.feeRate(_priority);
83 + return '${priority.toString()} ($rate sat/byte)';
84 + }
85 +
86 + return priority.toString();
87 + },
88 selectedItem: () => transactionPriority,
89 onItemSelected: (TransactionPriority priority) =>
90 _settingsStore.priority[wallet.type] = priority),
pubspec.yaml
+1 -1
@@ -11,7 +11,7 @@ description: Cake Wallet.
11 # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
12 # Read more about iOS versioning at
13 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
14 -version: 4.1.1+40
14 +version: 4.1.2+41
15
16 environment:
17 sdk: ">=2.7.0 <3.0.0"