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"