From aca8e8a292d413782821ba4bb48038384955a9ba Mon Sep 17 00:00:00 2001 From: OleksandrSobol Date: Tue, 25 May 2021 20:32:36 +0300 Subject: [PATCH 1/7] CAKE-329 | added unspent coins list page to the app --- lib/di.dart | 8 ++ lib/router.dart | 5 ++ lib/routes.dart | 1 + lib/src/screens/send/send_page.dart | 41 ++++++---- .../unspent_coins_list_page.dart | 38 +++++++++ .../widgets/unspent_coins_list_item.dart | 77 +++++++++++++++++++ .../unspent_coins/unspent_coins_item.dart | 14 ++++ .../unspent_coins_list_view_model.dart | 47 +++++++++++ pubspec.lock | 6 +- 9 files changed, 220 insertions(+), 17 deletions(-) create mode 100644 lib/src/screens/unspent_coins/unspent_coins_list_page.dart create mode 100644 lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart create mode 100644 lib/view_model/unspent_coins/unspent_coins_item.dart create mode 100644 lib/view_model/unspent_coins/unspent_coins_list_view_model.dart diff --git a/lib/di.dart b/lib/di.dart index cae3a068f..211bf5708 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -40,6 +40,7 @@ import 'package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart'; import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/trade_details/trade_details_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; +import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart'; @@ -76,6 +77,7 @@ import 'package:cake_wallet/view_model/setup_pin_code_view_model.dart'; import 'package:cake_wallet/view_model/support_view_model.dart'; import 'package:cake_wallet/view_model/transaction_details_view_model.dart'; import 'package:cake_wallet/view_model/trade_details_view_model.dart'; +import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/auth_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; @@ -561,5 +563,11 @@ Future setup( getIt.registerFactory(() => SupportPage(getIt.get())); + getIt.registerFactory(() => UnspentCoinsListViewModel()); + + getIt.registerFactory(() => UnspentCoinsListPage( + unspentCoinsListViewModel: getIt.get() + )); + _isSetupFinished = true; } diff --git a/lib/router.dart b/lib/router.dart index 19a6b760f..b8967645e 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -9,6 +9,7 @@ import 'package:cake_wallet/src/screens/restore/restore_from_backup_page.dart'; import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart'; import 'package:cake_wallet/src/screens/seed/pre_seed_page.dart'; import 'package:cake_wallet/src/screens/support/support_page.dart'; +import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; import 'package:cake_wallet/src/screens/wyre/wyre_page.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; @@ -355,6 +356,10 @@ Route createRoute(RouteSettings settings) { return CupertinoPageRoute( builder: (_) => getIt.get()); + case Routes.unspentCoinsList: + return MaterialPageRoute( + builder: (_) => getIt.get()); + default: return MaterialPageRoute( builder: (_) => Scaffold( diff --git a/lib/routes.dart b/lib/routes.dart index 2bf8ee7b7..84a48d5ea 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -54,4 +54,5 @@ class Routes { static const support = '/support'; static const orderDetails = '/order_details'; static const wyre = '/wyre'; + static const unspentCoinsList = '/unspent_coins_list'; } \ No newline at end of file diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 9fae0ec23..1fd74e130 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -558,22 +558,35 @@ class SendPage extends BasePage { ), bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - bottomSection: Observer(builder: (_) { - return LoadingPrimaryButton( - onPressed: () async { - if (_formKey.currentState.validate()) { - await sendViewModel.createTransaction(); - } - }, - text: S.of(context).send, - color: Theme.of(context).accentTextTheme.body2.color, - textColor: Colors.white, - isLoading: sendViewModel.state is IsExecutingState || - sendViewModel.state is TransactionCommitting, - isDisabled: + bottomSection: Column( + children: [ + Observer(builder: (_) { + return LoadingPrimaryButton( + onPressed: () async { + if (_formKey.currentState.validate()) { + await sendViewModel.createTransaction(); + } + }, + text: S.of(context).send, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + isLoading: sendViewModel.state is IsExecutingState || + sendViewModel.state is TransactionCommitting, + isDisabled: false // FIXME !(syncStore.status is SyncedSyncStatus), ); - })), + }), + Padding( + padding: EdgeInsets.only(top: 12), + child: PrimaryButton( + onPressed: () => Navigator.of(context).pushNamed(Routes.unspentCoinsList), + text: 'Unspent coins', + color: Colors.green, + textColor: Colors.white, + ) + ) + ], + )), )); } diff --git a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart new file mode 100644 index 000000000..44a6a34b8 --- /dev/null +++ b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart @@ -0,0 +1,38 @@ +import 'package:cake_wallet/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart'; +import 'package:cake_wallet/src/widgets/standard_list.dart'; +import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; + +class UnspentCoinsListPage extends BasePage { + UnspentCoinsListPage({this.unspentCoinsListViewModel}); + + @override + String get title => 'Unspent coins'; + + final UnspentCoinsListViewModel unspentCoinsListViewModel; + + @override + Widget body(BuildContext context) { + return SectionStandardList( + sectionCount: 1, + itemCounter: (int _) => unspentCoinsListViewModel.items.length, + itemBuilder: (_, __, index) { + final item = unspentCoinsListViewModel.items[index]; + + return GestureDetector( + onTap: () {print('Item taped');}, + child: UnspentCoinsListItem( + address: item.address, + amount: item.amount, + isFrozen: item.isFrozen, + note: item.note, + isSending: item.isSending, + onCheckBoxTap: (value) {print('CheckBox taped');}, + ) + ); + }); + } + +} \ No newline at end of file diff --git a/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart b/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart new file mode 100644 index 000000000..eb8ba33c0 --- /dev/null +++ b/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart @@ -0,0 +1,77 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; + +class UnspentCoinsListItem extends StatelessWidget { + UnspentCoinsListItem({ + @required this.address, + @required this.amount, + @required this.isFrozen, + @required this.note, + @required this.isSending, + @required this.onCheckBoxTap, +}); + + final String address; + final String amount; + final bool isFrozen; + final String note; + final bool isSending; + final Function(bool) onCheckBoxTap; + + @override + Widget build(BuildContext context) { + final textStyle = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .primaryTextTheme + .title + .color); + + return Container( + padding: EdgeInsets.fromLTRB(24, 16, 24, 16), + color: Theme.of(context).backgroundColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + address ?? 'Address', + style: textStyle, + ), + Padding( + padding: EdgeInsets.only(top: 12), + child: Text( + amount ?? 'Amount', + style: textStyle, + ) + ), + if (isFrozen ?? false) Padding( + padding: EdgeInsets.only(top: 12), + child: Text( + 'Freeze', + style: textStyle, + ) + ), + if (note?.isNotEmpty ?? false) Padding( + padding: EdgeInsets.only(top: 12), + child: AutoSizeText( + note, + style: textStyle, + maxLines: 1 + ) + ), + Padding( + padding: EdgeInsets.only(top: 12), + child: StandardCheckbox( + value: isSending, + caption: 'Sending', + onChanged: onCheckBoxTap + ) + ) + ], + ) + ); + } +} \ No newline at end of file diff --git a/lib/view_model/unspent_coins/unspent_coins_item.dart b/lib/view_model/unspent_coins/unspent_coins_item.dart new file mode 100644 index 000000000..94b7d55c6 --- /dev/null +++ b/lib/view_model/unspent_coins/unspent_coins_item.dart @@ -0,0 +1,14 @@ +class UnspentCoinsItem { + UnspentCoinsItem({ + this.address, + this.amount, + this.isFrozen, + this.note, + this.isSending = true}); + + final String address; + final String amount; + final bool isFrozen; + final String note; + final bool isSending; +} \ No newline at end of file diff --git a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart new file mode 100644 index 000000000..4b765d55e --- /dev/null +++ b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart @@ -0,0 +1,47 @@ +import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; +import 'package:mobx/mobx.dart'; + +part 'unspent_coins_list_view_model.g.dart'; + +const List> unspentCoinsMap = [ + { + "address" : "11111111111111121111132432432432432432432443124324324234324324324324332424", + "amount" : "222", + "isFrozen" : true, + "note" : "333cvgf23132132132132131321321314rwrtdggfdddewq ewqasfdxgdhgfgfszczcxgbhhhbcgbc"}, + { + "address" : "444", + "amount" : "555", + "note" : "sfjskf"}, + { + "address" : "777", + "amount" : "888", + "isFrozen" : false}, + { + "address" : "11111111111111121111132432432432432432432443124324324234324324324324332424", + "amount" : "222", + "isFrozen" : true, + "note" : "333cvgf23132132132132131321321314rwrtdggfdddewq ewqasfdxgdhgfgfszczcxgbhhhbcgbc"}, + { + "address" : "444", + "amount" : "555", + "note" : "sffsfsdsgs"}, + { + "address" : "777", + "amount" : "888", + "isFrozen" : false}, + {}, +]; + +class UnspentCoinsListViewModel = UnspentCoinsListViewModelBase with _$UnspentCoinsListViewModel; + +abstract class UnspentCoinsListViewModelBase with Store { + @computed + List get items => unspentCoinsMap.map((elem) => + UnspentCoinsItem( + address: elem["address"] as String, + amount: elem["amount"] as String, + isFrozen: elem["isFrozen"] as bool, + note: elem["note"] as String + )).toList(); +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 3ff596074..207f4b18a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -70,7 +70,7 @@ packages: path: "." ref: cake resolved-ref: "02fef082f20af13de00b4e64efb93a2c1e5e1cf2" - url: "git@github.com:cake-tech/bech32.git" + url: "https://github.com/cake-tech/bech32.git" source: git version: "0.2.0" bip32: @@ -92,8 +92,8 @@ packages: description: path: "." ref: cake - resolved-ref: b3ab2926c665f0e68b74a4a5f31059f7fcd817b7 - url: "git@github.com:cake-tech/bitcoin_flutter.git" + resolved-ref: cbabfd87b6ce3cae6051a3e86ddb56e7a934e188 + url: "https://github.com/cake-tech/bitcoin_flutter.git" source: git version: "2.0.2" boolean_selector: From b71e85b11259738b70ef5970cc605c8d9a3b7f17 Mon Sep 17 00:00:00 2001 From: OleksandrSobol Date: Wed, 26 May 2021 19:10:46 +0300 Subject: [PATCH 2/7] CAKE-329 | applied observable list to items in the unspent_coins_list_view_model.dart --- .../unspent_coins_list_page.dart | 24 +++++++++---------- .../unspent_coins_list_view_model.dart | 15 ++++++------ 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart index 44a6a34b8..cc175eb01 100644 --- a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart +++ b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_mod import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; class UnspentCoinsListPage extends BasePage { UnspentCoinsListPage({this.unspentCoinsListViewModel}); @@ -15,24 +16,23 @@ class UnspentCoinsListPage extends BasePage { @override Widget body(BuildContext context) { - return SectionStandardList( + return Observer(builder: (_) => SectionStandardList( sectionCount: 1, itemCounter: (int _) => unspentCoinsListViewModel.items.length, itemBuilder: (_, __, index) { final item = unspentCoinsListViewModel.items[index]; return GestureDetector( - onTap: () {print('Item taped');}, - child: UnspentCoinsListItem( - address: item.address, - amount: item.amount, - isFrozen: item.isFrozen, - note: item.note, - isSending: item.isSending, - onCheckBoxTap: (value) {print('CheckBox taped');}, - ) - ); - }); + onTap: () {print('Item taped');}, + child: UnspentCoinsListItem( + address: item.address, + amount: item.amount, + isFrozen: item.isFrozen, + note: item.note, + isSending: item.isSending, + onCheckBoxTap: (value) {print('CheckBox taped');}, + )); + })); } } \ No newline at end of file diff --git a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart index 4b765d55e..5f631e52a 100644 --- a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart @@ -37,11 +37,12 @@ class UnspentCoinsListViewModel = UnspentCoinsListViewModelBase with _$UnspentCo abstract class UnspentCoinsListViewModelBase with Store { @computed - List get items => unspentCoinsMap.map((elem) => - UnspentCoinsItem( - address: elem["address"] as String, - amount: elem["amount"] as String, - isFrozen: elem["isFrozen"] as bool, - note: elem["note"] as String - )).toList(); + ObservableList get items => + ObservableList.of(unspentCoinsMap.map((elem) => + UnspentCoinsItem( + address: elem["address"] as String, + amount: elem["amount"] as String, + isFrozen: elem["isFrozen"] as bool, + note: elem["note"] as String + ))); } \ No newline at end of file From e36c07b0da6377849f363c5fb94bd4f01bfd5af4 Mon Sep 17 00:00:00 2001 From: OleksandrSobol Date: Thu, 27 May 2021 20:57:51 +0300 Subject: [PATCH 3/7] CAKE-330 | added unspent coins details page to the app --- lib/di.dart | 12 ++++ lib/router.dart | 8 +++ lib/routes.dart | 1 + .../unspent_coins_details_page.dart | 52 ++++++++++++++++++ .../unspent_coins_list_page.dart | 5 +- .../widgets/unspent_coins_switch_row.dart | 45 +++++++++++++++ .../unspent_coins_details_view_model.dart | 55 +++++++++++++++++++ .../unspent_coins_switch_item.dart | 12 ++++ 8 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 lib/src/screens/unspent_coins/unspent_coins_details_page.dart create mode 100644 lib/src/screens/unspent_coins/widgets/unspent_coins_switch_row.dart create mode 100644 lib/view_model/unspent_coins/unspent_coins_details_view_model.dart create mode 100644 lib/view_model/unspent_coins/unspent_coins_switch_item.dart diff --git a/lib/di.dart b/lib/di.dart index 211bf5708..70f5b4a51 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -40,6 +40,7 @@ import 'package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart'; import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/trade_details/trade_details_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; +import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_page.dart'; @@ -77,6 +78,8 @@ import 'package:cake_wallet/view_model/setup_pin_code_view_model.dart'; import 'package:cake_wallet/view_model/support_view_model.dart'; import 'package:cake_wallet/view_model/transaction_details_view_model.dart'; import 'package:cake_wallet/view_model/trade_details_view_model.dart'; +import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_details_view_model.dart'; +import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/auth_view_model.dart'; @@ -569,5 +572,14 @@ Future setup( unspentCoinsListViewModel: getIt.get() )); + getIt.registerFactoryParam((item, _) => + UnspentCoinsDetailsViewModel(unspentCoinsItem: item)); + + getIt.registerFactoryParam((UnspentCoinsItem item, _) => + UnspentCoinsDetailsPage(unspentCoinsDetailsViewModel: + getIt.get(param1: item))); + _isSetupFinished = true; } diff --git a/lib/router.dart b/lib/router.dart index b8967645e..7ecb95026 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -9,10 +9,12 @@ import 'package:cake_wallet/src/screens/restore/restore_from_backup_page.dart'; import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart'; import 'package:cake_wallet/src/screens/seed/pre_seed_page.dart'; import 'package:cake_wallet/src/screens/support/support_page.dart'; +import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; import 'package:cake_wallet/src/screens/wyre/wyre_page.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; +import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; @@ -360,6 +362,12 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute( builder: (_) => getIt.get()); + case Routes.unspentCoinsDetails: + return MaterialPageRoute( + builder: (_) => + getIt.get( + param1: settings.arguments as UnspentCoinsItem)); + default: return MaterialPageRoute( builder: (_) => Scaffold( diff --git a/lib/routes.dart b/lib/routes.dart index 84a48d5ea..65f8253a8 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -55,4 +55,5 @@ class Routes { static const orderDetails = '/order_details'; static const wyre = '/wyre'; static const unspentCoinsList = '/unspent_coins_list'; + static const unspentCoinsDetails = '/unspent_coins_details'; } \ No newline at end of file diff --git a/lib/src/screens/unspent_coins/unspent_coins_details_page.dart b/lib/src/screens/unspent_coins/unspent_coins_details_page.dart new file mode 100644 index 000000000..9f73eaaa3 --- /dev/null +++ b/lib/src/screens/unspent_coins/unspent_coins_details_page.dart @@ -0,0 +1,52 @@ +import 'package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart'; +import 'package:cake_wallet/src/screens/transaction_details/widgets/textfield_list_row.dart'; +import 'package:cake_wallet/src/screens/unspent_coins/widgets/unspent_coins_switch_row.dart'; +import 'package:cake_wallet/src/widgets/standard_list.dart'; +import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_details_view_model.dart'; +import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_switch_item.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/src/widgets/standart_list_row.dart'; +import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class UnspentCoinsDetailsPage extends BasePage { + UnspentCoinsDetailsPage({this.unspentCoinsDetailsViewModel}); + + @override + String get title => 'Unspent coins details'; + + final UnspentCoinsDetailsViewModel unspentCoinsDetailsViewModel; + + @override + Widget body(BuildContext context) { + return SectionStandardList( + sectionCount: 1, + itemCounter: (int _) => unspentCoinsDetailsViewModel.items.length, + itemBuilder: (_, __, index) { + final item = unspentCoinsDetailsViewModel.items[index]; + + if (item is StandartListItem) { + return StandartListRow(title: '${item.title}:', value: item.value); + } + + if (item is TextFieldListItem) { + return TextFieldListRow( + title: item.title, + value: item.value, + onSubmitted: item.onSubmitted, + ); + } + + if (item is UnspentCoinsSwitchItem) { + return Observer(builder: (_) => UnspentCoinsSwitchRow( + title: item.title, + switchValue: item.switchValue(), + onSwitchValueChange: item.onSwitchValueChange + )); + } + + return null; + }); + } +} \ No newline at end of file diff --git a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart index cc175eb01..8a7b7de6c 100644 --- a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart +++ b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart'; import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; @@ -23,7 +24,9 @@ class UnspentCoinsListPage extends BasePage { final item = unspentCoinsListViewModel.items[index]; return GestureDetector( - onTap: () {print('Item taped');}, + onTap: () => + Navigator.of(context).pushNamed(Routes.unspentCoinsDetails, + arguments: item), child: UnspentCoinsListItem( address: item.address, amount: item.amount, diff --git a/lib/src/screens/unspent_coins/widgets/unspent_coins_switch_row.dart b/lib/src/screens/unspent_coins/widgets/unspent_coins_switch_row.dart new file mode 100644 index 000000000..c7c1e81b8 --- /dev/null +++ b/lib/src/screens/unspent_coins/widgets/unspent_coins_switch_row.dart @@ -0,0 +1,45 @@ +import 'package:cake_wallet/src/widgets/standart_switch.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class UnspentCoinsSwitchRow extends StatelessWidget { + UnspentCoinsSwitchRow( + {this.title, + this.titleFontSize = 14, + this.switchValue, + this.onSwitchValueChange}); + + final String title; + final double titleFontSize; + final bool switchValue; + final void Function(bool value) onSwitchValueChange; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + color: Theme.of(context).backgroundColor, + child: Padding( + padding: + const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: TextStyle( + fontSize: titleFontSize, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .primaryTextTheme.overline.color), + textAlign: TextAlign.left), + Padding( + padding: EdgeInsets.only(top: 12), + child: StandartSwitch( + value: switchValue, + onTaped: () => onSwitchValueChange(!switchValue)) + ) + ]), + ), + ); + } +} \ No newline at end of file diff --git a/lib/view_model/unspent_coins/unspent_coins_details_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_details_view_model.dart new file mode 100644 index 000000000..33db96eba --- /dev/null +++ b/lib/view_model/unspent_coins/unspent_coins_details_view_model.dart @@ -0,0 +1,55 @@ +import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; +import 'package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart'; +import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; +import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_switch_item.dart'; +import 'package:mobx/mobx.dart'; + +part 'unspent_coins_details_view_model.g.dart'; + +class UnspentCoinsDetailsViewModel = UnspentCoinsDetailsViewModelBase + with _$UnspentCoinsDetailsViewModel; + +abstract class UnspentCoinsDetailsViewModelBase with Store { + UnspentCoinsDetailsViewModelBase({this.unspentCoinsItem}) { + final amount = unspentCoinsItem.amount ?? ''; + final address = unspentCoinsItem.address ?? ''; + isFrozen = unspentCoinsItem.isFrozen ?? false; + note = unspentCoinsItem.note ?? ''; + + items = [ + StandartListItem( + title: 'Amount', + value: amount + ), + StandartListItem( + title: 'Address', + value: address + ), + TextFieldListItem( + title: S.current.note_tap_to_change, + value: note, + onSubmitted: (value) { + note = value; + }), + UnspentCoinsSwitchItem( + title: 'Freeze', + value: '', + switchValue: () => isFrozen, + onSwitchValueChange: (value) { + isFrozen = value; + } + ) + ]; + } + + @observable + bool isFrozen; + + @observable + String note; + + final UnspentCoinsItem unspentCoinsItem; + List items; +} \ No newline at end of file diff --git a/lib/view_model/unspent_coins/unspent_coins_switch_item.dart b/lib/view_model/unspent_coins/unspent_coins_switch_item.dart new file mode 100644 index 000000000..925f8776e --- /dev/null +++ b/lib/view_model/unspent_coins/unspent_coins_switch_item.dart @@ -0,0 +1,12 @@ +import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart'; + +class UnspentCoinsSwitchItem extends TransactionDetailsListItem { + UnspentCoinsSwitchItem({ + String title, + String value, + this.switchValue, + this.onSwitchValueChange}) : super(title: title, value: value); + + final bool Function() switchValue; + final void Function(bool value) onSwitchValueChange; +} \ No newline at end of file From b1add664ad5b762618190c3ad8e9e5f4c24600bf Mon Sep 17 00:00:00 2001 From: OleksandrSobol Date: Mon, 31 May 2021 21:04:00 +0300 Subject: [PATCH 4/7] CAKE-329 | reworked unspent_coins_list_page.dart and unspent_coins_list_item.dart; fixed standard_checkbox.dart; applied unspent coins control only to btc wallet --- lib/palette.dart | 1 + lib/src/screens/send/send_page.dart | 71 +++++----- .../unspent_coins_list_page.dart | 70 +++++++--- .../widgets/unspent_coins_list_item.dart | 121 +++++++++++------- lib/src/widgets/standard_checkbox.dart | 22 ++-- lib/view_model/send/send_view_model.dart | 5 + .../unspent_coins_list_view_model.dart | 37 ++++-- 7 files changed, 209 insertions(+), 118 deletions(-) diff --git a/lib/palette.dart b/lib/palette.dart index 336f7b7f9..301669a24 100644 --- a/lib/palette.dart +++ b/lib/palette.dart @@ -44,6 +44,7 @@ class Palette { static const Color dullGray = Color.fromRGBO(98, 98, 98, 1.0); static const Color protectiveBlue = Color.fromRGBO(33, 148, 255, 1.0); static const Color darkBlue = Color.fromRGBO(109, 128, 178, 1.0); + static const Color paleCornflowerBlue = Color.fromRGBO(185, 196, 237, 1.0); } class PaletteDark { diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 1fd74e130..22ba76836 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -425,7 +425,31 @@ class SendPage extends BasePage { ], ), ), - )) + )), + if (sendViewModel.isBitcoinWallet) Padding( + padding: EdgeInsets.only(top: 6), + child: GestureDetector( + onTap: () => Navigator.of(context) + .pushNamed(Routes.unspentCoinsList), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Coin control (optional)', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.white)), + Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white, + ) + ], + ) + ) + ) ], ), ) @@ -558,35 +582,22 @@ class SendPage extends BasePage { ), bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - bottomSection: Column( - children: [ - Observer(builder: (_) { - return LoadingPrimaryButton( - onPressed: () async { - if (_formKey.currentState.validate()) { - await sendViewModel.createTransaction(); - } - }, - text: S.of(context).send, - color: Theme.of(context).accentTextTheme.body2.color, - textColor: Colors.white, - isLoading: sendViewModel.state is IsExecutingState || - sendViewModel.state is TransactionCommitting, - isDisabled: - false // FIXME !(syncStore.status is SyncedSyncStatus), - ); - }), - Padding( - padding: EdgeInsets.only(top: 12), - child: PrimaryButton( - onPressed: () => Navigator.of(context).pushNamed(Routes.unspentCoinsList), - text: 'Unspent coins', - color: Colors.green, - textColor: Colors.white, - ) - ) - ], - )), + bottomSection: Observer(builder: (_) { + return LoadingPrimaryButton( + onPressed: () async { + if (_formKey.currentState.validate()) { + await sendViewModel.createTransaction(); + } + }, + text: S.of(context).send, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + isLoading: sendViewModel.state is IsExecutingState || + sendViewModel.state is TransactionCommitting, + isDisabled: + false // FIXME !(syncStore.status is SyncedSyncStatus), + ); + })), )); } diff --git a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart index cc175eb01..81dbaf104 100644 --- a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart +++ b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart @@ -1,10 +1,12 @@ import 'package:cake_wallet/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart'; -import 'package:cake_wallet/src/widgets/standard_list.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:cake_wallet/generated/i18n.dart'; class UnspentCoinsListPage extends BasePage { UnspentCoinsListPage({this.unspentCoinsListViewModel}); @@ -12,27 +14,59 @@ class UnspentCoinsListPage extends BasePage { @override String get title => 'Unspent coins'; + @override + Widget trailing(BuildContext context) { + final questionImage = Image.asset('assets/images/question_mark.png', + color: Theme.of(context).primaryTextTheme.title.color); + + return SizedBox( + height: 20.0, + width: 20.0, + child: ButtonTheme( + minWidth: double.minPositive, + child: FlatButton( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + padding: EdgeInsets.all(0), + onPressed: () => showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: '', + alertContent: 'Information about unspent coins', + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }), + child: questionImage), + ), + ); + } + final UnspentCoinsListViewModel unspentCoinsListViewModel; @override Widget body(BuildContext context) { - return Observer(builder: (_) => SectionStandardList( - sectionCount: 1, - itemCounter: (int _) => unspentCoinsListViewModel.items.length, - itemBuilder: (_, __, index) { - final item = unspentCoinsListViewModel.items[index]; + return Container( + padding: EdgeInsets.fromLTRB(24, 12, 24, 24), + child: Observer( + builder: (_) => ListView.separated( + itemCount: unspentCoinsListViewModel.items.length, + separatorBuilder: (_, __) => + SizedBox(height: 15), + itemBuilder: (_, int index) { + final item = unspentCoinsListViewModel.items[index]; - return GestureDetector( - onTap: () {print('Item taped');}, - child: UnspentCoinsListItem( - address: item.address, - amount: item.amount, - isFrozen: item.isFrozen, - note: item.note, - isSending: item.isSending, - onCheckBoxTap: (value) {print('CheckBox taped');}, - )); - })); + return GestureDetector( + onTap: () {print('Item taped');}, + child: UnspentCoinsListItem( + address: item.address, + amount: item.amount, + isSending: item.isSending, + onCheckBoxTap: (value) {}, + )); + } + ) + ) + ); } - } \ No newline at end of file diff --git a/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart b/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart index eb8ba33c0..790e19c77 100644 --- a/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart +++ b/lib/src/screens/unspent_coins/widgets/unspent_coins_list_item.dart @@ -1,74 +1,101 @@ import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; -class UnspentCoinsListItem extends StatelessWidget { +class UnspentCoinsListItem extends StatefulWidget { UnspentCoinsListItem({ @required this.address, @required this.amount, - @required this.isFrozen, - @required this.note, @required this.isSending, @required this.onCheckBoxTap, -}); + }); final String address; final String amount; - final bool isFrozen; - final String note; final bool isSending; final Function(bool) onCheckBoxTap; + @override UnspentCoinsListItemState createState() => + UnspentCoinsListItemState( + address: address, + amount: amount, + isSending: isSending, + onCheckBoxTap: onCheckBoxTap + ); + +} + +class UnspentCoinsListItemState extends State { + UnspentCoinsListItemState({ + @required this.address, + @required this.amount, + @required this.isSending, + @required this.onCheckBoxTap, + }) : checkBoxValue = isSending; + + static const amountColor = Palette.darkBlueCraiola; + static const addressColor = Palette.darkGray; + static const selectedItemColor = Palette.paleCornflowerBlue; + static const unselectedItemColor = Palette.moderateLavender; + + final String address; + final String amount; + final bool isSending; + final Function(bool) onCheckBoxTap; + + bool checkBoxValue; + @override Widget build(BuildContext context) { - final textStyle = TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context) - .primaryTextTheme - .title - .color); + final itemColor = checkBoxValue? selectedItemColor : unselectedItemColor; return Container( - padding: EdgeInsets.fromLTRB(24, 16, 24, 16), - color: Theme.of(context).backgroundColor, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + height: 62, + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: itemColor), + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - address ?? 'Address', - style: textStyle, - ), Padding( - padding: EdgeInsets.only(top: 12), - child: Text( - amount ?? 'Amount', - style: textStyle, - ) - ), - if (isFrozen ?? false) Padding( - padding: EdgeInsets.only(top: 12), - child: Text( - 'Freeze', - style: textStyle, - ) - ), - if (note?.isNotEmpty ?? false) Padding( - padding: EdgeInsets.only(top: 12), - child: AutoSizeText( - note, - style: textStyle, - maxLines: 1 - ) - ), - Padding( - padding: EdgeInsets.only(top: 12), + padding: EdgeInsets.only(right: 12), child: StandardCheckbox( - value: isSending, - caption: 'Sending', - onChanged: onCheckBoxTap + value: checkBoxValue, + onChanged: (value) { + onCheckBoxTap(value); + checkBoxValue = value; + setState(() {}); + } ) + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AutoSizeText( + amount ?? 'Amount', + style: TextStyle( + color: amountColor, + fontSize: 16, + fontWeight: FontWeight.w600 + ), + maxLines: 1, + ), + AutoSizeText( + address ?? 'Address', + style: TextStyle( + color: addressColor, + fontSize: 12, + ), + maxLines: 1, + ) + ] + ) ) ], ) diff --git a/lib/src/widgets/standard_checkbox.dart b/lib/src/widgets/standard_checkbox.dart index 64ae95acc..9deec340b 100644 --- a/lib/src/widgets/standard_checkbox.dart +++ b/lib/src/widgets/standard_checkbox.dart @@ -44,9 +44,6 @@ class StandardCheckboxState extends State { Container( height: 24.0, width: 24.0, - margin: EdgeInsets.only( - right: 10.0, - ), decoration: BoxDecoration( border: Border.all( color: Theme.of(context) @@ -65,14 +62,17 @@ class StandardCheckboxState extends State { ) : Offstage(), ), - Text( - caption, - style: TextStyle( - fontSize: 16.0, - color: Theme.of(context) - .primaryTextTheme - .title - .color), + if (caption.isNotEmpty) Padding( + padding: EdgeInsets.only(left: 10), + child: Text( + caption, + style: TextStyle( + fontSize: 16.0, + color: Theme.of(context) + .primaryTextTheme + .title + .color), + ) ) ], ), diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 689032223..a9e6aaf93 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -55,6 +55,8 @@ abstract class SendViewModelBase with Store { _settingsStore.priority[_wallet.type] = priorities.first; } + isBitcoinWallet = _wallet is BitcoinWallet; + _setCryptoNumMaximumFractionDigits(); } @@ -182,6 +184,9 @@ abstract class SendViewModelBase with Store { @observable PendingTransaction pendingTransaction; + @observable + bool isBitcoinWallet; + @computed String get balance => _wallet.balance.formattedAvailableBalance ?? '0.0'; diff --git a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart index 5f631e52a..25167f785 100644 --- a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart @@ -5,32 +5,45 @@ part 'unspent_coins_list_view_model.g.dart'; const List> unspentCoinsMap = [ { - "address" : "11111111111111121111132432432432432432432443124324324234324324324324332424", - "amount" : "222", + "address" : "bc1qm80mu5p3mf04a7cj7teymasf04dwpc3av2fwtr", + "amount" : "0.00358 BTC", "isFrozen" : true, "note" : "333cvgf23132132132132131321321314rwrtdggfdddewq ewqasfdxgdhgfgfszczcxgbhhhbcgbc"}, { - "address" : "444", - "amount" : "555", + "address" : "bc1qm80mu5p3mf04a7cj7teymasf04dwpc3av2fwtr", + "amount" : "0.00567894 BTC", "note" : "sfjskf"}, { - "address" : "777", - "amount" : "888", + "address" : "bc1qm80mu5p3mf04a7cj7teymasf04dwpc3av2fwtr", + "amount" : "0.00087 BTC", "isFrozen" : false}, { - "address" : "11111111111111121111132432432432432432432443124324324234324324324324332424", - "amount" : "222", + "address" : "bc1qm80mu5p3mf04a7cj7teymasf04dwpc3av2fwtr", + "amount" : "0.00012 BTC", "isFrozen" : true, "note" : "333cvgf23132132132132131321321314rwrtdggfdddewq ewqasfdxgdhgfgfszczcxgbhhhbcgbc"}, { - "address" : "444", - "amount" : "555", + "address" : "bc1qm80mu5p3mf04a7cj7teymasf04dwpc3av2fwtr", + "amount" : "0.00574 BTC", "note" : "sffsfsdsgs"}, { - "address" : "777", - "amount" : "888", + "address" : "bc1qm80mu5p3mf04a7cj7teymasf04dwpc3av2fwtr", + "amount" : "0.000482 BTC", "isFrozen" : false}, {}, + { + "address" : "bc1qm80mu5p3mf04a7cj7teymasf04dwpc3av2fwtr", + "amount" : "0.00012 BTC", + "isFrozen" : true, + "note" : "333cvgf23132132132132131321321314rwrtdggfdddewq ewqasfdxgdhgfgfszczcxgbhhhbcgbc"}, + { + "address" : "bc1qm80mu5p3mf04a7cj7teymasf04dwpc3av2fwtr", + "amount" : "0.00574 BTC", + "note" : "sffsfsdsgs"}, + { + "address" : "bc1qm80mu5p3mf04a7cj7teymasf04dwpc3av2fwtr", + "amount" : "0.000482 BTC", + "isFrozen" : false}, ]; class UnspentCoinsListViewModel = UnspentCoinsListViewModelBase with _$UnspentCoinsListViewModel; From 61375468a5ff43f964de4d77650faa92b7030143 Mon Sep 17 00:00:00 2001 From: OleksandrSobol Date: Wed, 2 Jun 2021 17:36:12 +0300 Subject: [PATCH 5/7] CAKE-329 | show unspent coins alert when unspent coins list page is started --- .../unspent_coins_list_page.dart | 92 +++++++++++++------ 1 file changed, 63 insertions(+), 29 deletions(-) diff --git a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart index 81dbaf104..d5c5eff05 100644 --- a/lib/src/screens/unspent_coins/unspent_coins_list_page.dart +++ b/lib/src/screens/unspent_coins/unspent_coins_list_page.dart @@ -28,15 +28,7 @@ class UnspentCoinsListPage extends BasePage { highlightColor: Colors.transparent, splashColor: Colors.transparent, padding: EdgeInsets.all(0), - onPressed: () => showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: '', - alertContent: 'Information about unspent coins', - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }), + onPressed: () => showUnspentCoinsAlert(context), child: questionImage), ), ); @@ -45,28 +37,70 @@ class UnspentCoinsListPage extends BasePage { final UnspentCoinsListViewModel unspentCoinsListViewModel; @override - Widget body(BuildContext context) { - return Container( - padding: EdgeInsets.fromLTRB(24, 12, 24, 24), - child: Observer( - builder: (_) => ListView.separated( - itemCount: unspentCoinsListViewModel.items.length, - separatorBuilder: (_, __) => - SizedBox(height: 15), - itemBuilder: (_, int index) { - final item = unspentCoinsListViewModel.items[index]; + Widget body(BuildContext context) => + UnspentCoinsListForm(unspentCoinsListViewModel); +} - return GestureDetector( - onTap: () {print('Item taped');}, - child: UnspentCoinsListItem( - address: item.address, - amount: item.amount, - isSending: item.isSending, - onCheckBoxTap: (value) {}, - )); - } +class UnspentCoinsListForm extends StatefulWidget { + UnspentCoinsListForm(this.unspentCoinsListViewModel); + + final UnspentCoinsListViewModel unspentCoinsListViewModel; + + @override + UnspentCoinsListFormState createState() => + UnspentCoinsListFormState(unspentCoinsListViewModel); +} + +class UnspentCoinsListFormState extends State { + UnspentCoinsListFormState(this.unspentCoinsListViewModel); + + final UnspentCoinsListViewModel unspentCoinsListViewModel; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback(afterLayout); + } + + void afterLayout(dynamic _) { + showUnspentCoinsAlert(context); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.fromLTRB(24, 12, 24, 24), + child: Observer( + builder: (_) => ListView.separated( + itemCount: unspentCoinsListViewModel.items.length, + separatorBuilder: (_, __) => + SizedBox(height: 15), + itemBuilder: (_, int index) { + final item = unspentCoinsListViewModel.items[index]; + + return GestureDetector( + onTap: () {print('Item taped');}, + child: UnspentCoinsListItem( + address: item.address, + amount: item.amount, + isSending: item.isSending, + onCheckBoxTap: (value) {}, + )); + } + ) ) - ) ); } +} + +void showUnspentCoinsAlert(BuildContext context) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: '', + alertContent: 'Information about unspent coins', + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); } \ No newline at end of file From 52b1abcbdb47be080b31b615685589a19191126a Mon Sep 17 00:00:00 2001 From: OleksandrSobol Date: Wed, 2 Jun 2021 19:54:25 +0300 Subject: [PATCH 6/7] CAKE-330 | merged CAKE-329 branch into current --- lib/src/screens/unspent_coins/unspent_coins_details_page.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/screens/unspent_coins/unspent_coins_details_page.dart b/lib/src/screens/unspent_coins/unspent_coins_details_page.dart index 9f73eaaa3..f5e0f8991 100644 --- a/lib/src/screens/unspent_coins/unspent_coins_details_page.dart +++ b/lib/src/screens/unspent_coins/unspent_coins_details_page.dart @@ -27,7 +27,9 @@ class UnspentCoinsDetailsPage extends BasePage { final item = unspentCoinsDetailsViewModel.items[index]; if (item is StandartListItem) { - return StandartListRow(title: '${item.title}:', value: item.value); + return StandartListRow( + title: '${item.title}:', + value: item.value); } if (item is TextFieldListItem) { From 20e0c830cf6d6703fc00d2faeec23f4404a04321 Mon Sep 17 00:00:00 2001 From: OleksandrSobol Date: Mon, 5 Jul 2021 16:52:24 +0300 Subject: [PATCH 7/7] CAKE-334 | applied unspent coins control to the app; added unspent_coins_info.dart; reworked createTransaction(), calculateEstimatedFee() and updateUnspent() methods in the electrum_wallet.dart; fixed unspent_coins_list_view_model.dart, unspent_coins_details_view_model.dart, unspent_coins_list_item.dart, unspent_coins_list_page.dart and unspent_coins_details_page.dart; fixed bitcoin_transaction_wrong_balance_exception.dart; added properties to bitcoin_unspent.dart; applied localization to unspent coins pages --- ...n_transaction_wrong_balance_exception.dart | 8 +- lib/bitcoin/bitcoin_unspent.dart | 8 +- lib/bitcoin/bitcoin_wallet.dart | 6 + lib/bitcoin/bitcoin_wallet_service.dart | 13 +- lib/bitcoin/electrum_wallet.dart | 185 +++++++++++++----- lib/bitcoin/litecoin_wallet.dart | 6 + lib/bitcoin/litecoin_wallet_service.dart | 13 +- lib/bitcoin/unspent_coins_info.dart | 32 +++ lib/di.dart | 41 +++- lib/main.dart | 12 +- lib/router.dart | 4 +- lib/src/screens/send/send_page.dart | 4 +- .../unspent_coins_details_page.dart | 3 +- .../unspent_coins_list_page.dart | 31 +-- .../widgets/unspent_coins_list_item.dart | 138 ++++++------- lib/src/widgets/standart_switch.dart | 1 - lib/view_model/send/send_view_model.dart | 6 +- .../unspent_coins_details_view_model.dart | 22 ++- .../unspent_coins/unspent_coins_item.dart | 35 +++- .../unspent_coins_list_view_model.dart | 97 +++++---- res/values/strings_de.arb | 7 +- res/values/strings_en.arb | 7 +- res/values/strings_es.arb | 7 +- res/values/strings_hi.arb | 7 +- res/values/strings_hr.arb | 7 +- res/values/strings_it.arb | 7 +- res/values/strings_ja.arb | 7 +- res/values/strings_ko.arb | 7 +- res/values/strings_nl.arb | 7 +- res/values/strings_pl.arb | 7 +- res/values/strings_pt.arb | 7 +- res/values/strings_ru.arb | 7 +- res/values/strings_uk.arb | 7 +- res/values/strings_zh.arb | 7 +- 34 files changed, 525 insertions(+), 238 deletions(-) create mode 100644 lib/bitcoin/unspent_coins_info.dart diff --git a/lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart b/lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart index 9d1401818..b699ade29 100644 --- a/lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart +++ b/lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart @@ -1,4 +1,10 @@ +import 'package:cake_wallet/entities/crypto_currency.dart'; + class BitcoinTransactionWrongBalanceException implements Exception { + BitcoinTransactionWrongBalanceException(this.currency); + + final CryptoCurrency currency; + @override - String toString() => 'Wrong balance. Not enough BTC on your balance.'; + String toString() => 'Wrong balance. Not enough ${currency.title} on your balance.'; } \ No newline at end of file diff --git a/lib/bitcoin/bitcoin_unspent.dart b/lib/bitcoin/bitcoin_unspent.dart index 846eb8c7d..b95ed9bc3 100644 --- a/lib/bitcoin/bitcoin_unspent.dart +++ b/lib/bitcoin/bitcoin_unspent.dart @@ -1,7 +1,10 @@ import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart'; class BitcoinUnspent { - BitcoinUnspent(this.address, this.hash, this.value, this.vout); + BitcoinUnspent(this.address, this.hash, this.value, this.vout) + : isSending = true, + isFrozen = false, + note = ''; factory BitcoinUnspent.fromJSON( BitcoinAddressRecord address, Map json) => @@ -15,4 +18,7 @@ class BitcoinUnspent { bool get isP2wpkh => address.address.startsWith('bc') || address.address.startsWith('ltc'); + bool isSending; + bool isFrozen; + String note; } diff --git a/lib/bitcoin/bitcoin_wallet.dart b/lib/bitcoin/bitcoin_wallet.dart index fd8402887..022ed478d 100644 --- a/lib/bitcoin/bitcoin_wallet.dart +++ b/lib/bitcoin/bitcoin_wallet.dart @@ -1,3 +1,5 @@ +import 'package:cake_wallet/bitcoin/unspent_coins_info.dart'; +import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter/foundation.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; @@ -17,6 +19,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { {@required String mnemonic, @required String password, @required WalletInfo walletInfo, + @required Box unspentCoinsInfo, List initialAddresses, ElectrumBalance initialBalance, int accountIndex = 0}) @@ -24,6 +27,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { mnemonic: mnemonic, password: password, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, networkType: bitcoin.bitcoin, initialAddresses: initialAddresses, initialBalance: initialBalance, @@ -32,6 +36,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { static Future open({ @required String name, @required WalletInfo walletInfo, + @required Box unspentCoinsInfo, @required String password, }) async { final snp = ElectrumWallletSnapshot(name, walletInfo.type, password); @@ -40,6 +45,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { mnemonic: snp.mnemonic, password: password, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp.addresses, initialBalance: snp.balance, accountIndex: snp.accountIndex); diff --git a/lib/bitcoin/bitcoin_wallet_service.dart b/lib/bitcoin/bitcoin_wallet_service.dart index aefe0fadf..8e5b31930 100644 --- a/lib/bitcoin/bitcoin_wallet_service.dart +++ b/lib/bitcoin/bitcoin_wallet_service.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart'; import 'package:cake_wallet/bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart'; +import 'package:cake_wallet/bitcoin/unspent_coins_info.dart'; import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/core/wallet_service.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; @@ -14,9 +15,10 @@ class BitcoinWalletService extends WalletService< BitcoinNewWalletCredentials, BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials> { - BitcoinWalletService(this.walletInfoSource); + BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; + final Box unspentCoinsInfoSource; @override WalletType getType() => WalletType.bitcoin; @@ -26,7 +28,8 @@ class BitcoinWalletService extends WalletService< final wallet = BitcoinWallet( mnemonic: await generateMnemonic(), password: credentials.password, - walletInfo: credentials.walletInfo); + walletInfo: credentials.walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); await wallet.save(); await wallet.init(); return wallet; @@ -42,7 +45,8 @@ class BitcoinWalletService extends WalletService< (info) => info.id == WalletBase.idFor(name, getType()), orElse: () => null); final wallet = await BitcoinWalletBase.open( - password: password, name: name, walletInfo: walletInfo); + password: password, name: name, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); await wallet.init(); return wallet; } @@ -67,7 +71,8 @@ class BitcoinWalletService extends WalletService< final wallet = BitcoinWallet( password: credentials.password, mnemonic: credentials.mnemonic, - walletInfo: credentials.walletInfo); + walletInfo: credentials.walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); await wallet.save(); await wallet.init(); return wallet; diff --git a/lib/bitcoin/electrum_wallet.dart b/lib/bitcoin/electrum_wallet.dart index bf3dfd542..43c5cd179 100644 --- a/lib/bitcoin/electrum_wallet.dart +++ b/lib/bitcoin/electrum_wallet.dart @@ -1,5 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:cake_wallet/bitcoin/unspent_coins_info.dart'; +import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; import 'package:flutter/foundation.dart'; @@ -38,6 +40,7 @@ abstract class ElectrumWalletBase extends WalletBase unspentCoinsInfo, @required List initialAddresses, @required this.networkType, @required this.mnemonic, @@ -59,9 +62,10 @@ abstract class ElectrumWalletBase extends WalletBase unspentCoinsInfo; @override @observable @@ -103,7 +108,7 @@ abstract class ElectrumWalletBase extends WalletBase _unspent; + List unspentCoins; List _feeRates; int _accountIndex; Map> _scripthashesUpdateSubject; @@ -178,10 +183,10 @@ abstract class ElectrumWalletBase extends WalletBase startSync() async { try { syncStatus = StartingSyncStatus(); - updateTransactions(); + await updateTransactions(); _subscribeForUpdates(); await _updateBalance(); - await _updateUnspent(); + await updateUnspent(); _feeRates = await electrumClient.feeRates(); Timer.periodic(const Duration(minutes: 1), @@ -218,33 +223,16 @@ abstract class ElectrumWalletBase extends WalletBase[]; - final allAmountFee = - calculateEstimatedFee(transactionCredentials.priority, null); - final allAmount = balance.confirmed - allAmountFee; - var fee = 0; - final credentialsAmount = transactionCredentials.amount != null - ? stringDoubleToBitcoinAmount(transactionCredentials.amount) - : 0; - final amount = transactionCredentials.amount == null || - allAmount - credentialsAmount < minAmount - ? allAmount - : credentialsAmount; - final txb = bitcoin.TransactionBuilder(network: networkType); - final changeAddress = address; - var leftAmount = amount; - var totalInputAmount = 0; + var allInputsAmount = 0; - if (_unspent.isEmpty) { - await _updateUnspent(); + if (unspentCoins.isEmpty) { + await updateUnspent(); } - for (final utx in _unspent) { - leftAmount = leftAmount - utx.value; - totalInputAmount += utx.value; - inputs.add(utx); - - if (leftAmount <= 0) { - break; + for (final utx in unspentCoins) { + if (utx.isSending) { + allInputsAmount += utx.value; + inputs.add(utx); } } @@ -252,18 +240,57 @@ abstract class ElectrumWalletBase extends WalletBase balance.confirmed) { - throw BitcoinTransactionWrongBalanceException(); + final credentialsAmount = transactionCredentials.amount != null + ? stringDoubleToBitcoinAmount(transactionCredentials.amount) + : 0; + final amount = transactionCredentials.amount == null || + allAmount - credentialsAmount < minAmount + ? allAmount + : credentialsAmount; + final fee = transactionCredentials.amount == null || amount == allAmount + ? allAmountFee + : calculateEstimatedFee(transactionCredentials.priority, amount); + + if (fee == 0) { + throw BitcoinTransactionWrongBalanceException(currency); } - if (amount <= 0 || totalInputAmount < amount) { - throw BitcoinTransactionWrongBalanceException(); + final totalAmount = amount + fee; + + if (totalAmount > balance.confirmed || totalAmount > allInputsAmount) { + throw BitcoinTransactionWrongBalanceException(currency); + } + + final txb = bitcoin.TransactionBuilder(network: networkType); + final changeAddress = address; + + var leftAmount = totalAmount; + var totalInputAmount = 0; + + inputs.clear(); + + for (final utx in unspentCoins) { + if (utx.isSending) { + leftAmount = leftAmount - utx.value; + totalInputAmount += utx.value; + inputs.add(utx); + + if (leftAmount <= 0) { + break; + } + } + } + + if (inputs.isEmpty) { + throw BitcoinTransactionNoInputsException(); + } + + if (amount <= 0 || totalInputAmount < totalAmount) { + throw BitcoinTransactionWrongBalanceException(currency); } txb.setVersion(1); @@ -338,17 +365,26 @@ abstract class ElectrumWalletBase extends WalletBase= amount) { break; } - totalValue += input.value; - inputsCount += 1; + if (input.isSending) { + totalValue += input.value; + inputsCount += 1; + } } + + if (totalValue < amount) return 0; } else { - inputsCount = _unspent.length; + for (final input in unspentCoins) { + if (input.isSending) { + inputsCount += 1; + } + } } + // If send all, then we have no change value return feeAmountForPriority( priority, inputsCount, amount != null ? 2 : 1); @@ -382,12 +418,73 @@ abstract class ElectrumWalletBase extends WalletBase makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); - Future _updateUnspent() async { + Future updateUnspent() async { final unspent = await Future.wait(addresses.map((address) => electrumClient .getListUnspentWithAddress(address.address, networkType) .then((unspent) => unspent .map((unspent) => BitcoinUnspent.fromJSON(address, unspent))))); - _unspent = unspent.expand((e) => e).toList(); + unspentCoins = unspent.expand((e) => e).toList(); + + if (unspentCoinsInfo.isEmpty) { + unspentCoins.forEach((coin) => _addCoinInfo(coin)); + return; + } + + if (unspentCoins.isNotEmpty) { + unspentCoins.forEach((coin) { + final coinInfoList = unspentCoinsInfo.values.where((element) => + element.walletId.contains(id) && element.hash.contains(coin.hash)); + + if (coinInfoList.isNotEmpty) { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + } else { + _addCoinInfo(coin); + } + }); + } + + await _refreshUnspentCoinsInfo(); + } + + Future _addCoinInfo(BitcoinUnspent coin) async { + final newInfo = UnspentCoinsInfo( + walletId: id, + hash: coin.hash, + isFrozen: coin.isFrozen, + isSending: coin.isSending, + note: coin.note + ); + + await unspentCoinsInfo.add(newInfo); + } + + Future _refreshUnspentCoinsInfo() async { + try { + final List keys = []; + final currentWalletUnspentCoins = unspentCoinsInfo.values + .where((element) => element.walletId.contains(id)); + + if (currentWalletUnspentCoins.isNotEmpty) { + currentWalletUnspentCoins.forEach((element) { + final existUnspentCoins = unspentCoins + ?.where((coin) => element.hash.contains(coin?.hash)); + + if (existUnspentCoins?.isEmpty ?? true) { + keys.add(element.key); + } + }); + } + + if (keys.isNotEmpty) { + await unspentCoinsInfo.deleteAll(keys); + } + } catch (e) { + print(e.toString()); + } } Future fetchTransactionInfo( @@ -438,7 +535,7 @@ abstract class ElectrumWalletBase extends WalletBase unspentCoinsInfo, List initialAddresses, ElectrumBalance initialBalance, int accountIndex = 0}) @@ -28,6 +31,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { mnemonic: mnemonic, password: password, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, networkType: litecoinNetwork, initialAddresses: initialAddresses, initialBalance: initialBalance, @@ -36,6 +40,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { static Future open({ @required String name, @required WalletInfo walletInfo, + @required Box unspentCoinsInfo, @required String password, }) async { final snp = ElectrumWallletSnapshot(name, walletInfo.type, password); @@ -44,6 +49,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { mnemonic: snp.mnemonic, password: password, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp.addresses, initialBalance: snp.balance, accountIndex: snp.accountIndex); diff --git a/lib/bitcoin/litecoin_wallet_service.dart b/lib/bitcoin/litecoin_wallet_service.dart index 053fd785f..dc5081e12 100644 --- a/lib/bitcoin/litecoin_wallet_service.dart +++ b/lib/bitcoin/litecoin_wallet_service.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:cake_wallet/bitcoin/unspent_coins_info.dart'; import 'package:hive/hive.dart'; import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart'; import 'package:cake_wallet/bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart'; @@ -14,9 +15,10 @@ class LitecoinWalletService extends WalletService< BitcoinNewWalletCredentials, BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials> { - LitecoinWalletService(this.walletInfoSource); + LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; + final Box unspentCoinsInfoSource; @override WalletType getType() => WalletType.litecoin; @@ -26,7 +28,8 @@ class LitecoinWalletService extends WalletService< final wallet = LitecoinWallet( mnemonic: await generateMnemonic(), password: credentials.password, - walletInfo: credentials.walletInfo); + walletInfo: credentials.walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); await wallet.save(); await wallet.init(); @@ -43,7 +46,8 @@ class LitecoinWalletService extends WalletService< (info) => info.id == WalletBase.idFor(name, getType()), orElse: () => null); final wallet = await LitecoinWalletBase.open( - password: password, name: name, walletInfo: walletInfo); + password: password, name: name, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); await wallet.init(); return wallet; } @@ -68,7 +72,8 @@ class LitecoinWalletService extends WalletService< final wallet = LitecoinWallet( password: credentials.password, mnemonic: credentials.mnemonic, - walletInfo: credentials.walletInfo); + walletInfo: credentials.walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); await wallet.save(); await wallet.init(); return wallet; diff --git a/lib/bitcoin/unspent_coins_info.dart b/lib/bitcoin/unspent_coins_info.dart new file mode 100644 index 000000000..6ce647dce --- /dev/null +++ b/lib/bitcoin/unspent_coins_info.dart @@ -0,0 +1,32 @@ +import 'package:hive/hive.dart'; + +part 'unspent_coins_info.g.dart'; + +@HiveType(typeId: UnspentCoinsInfo.typeId) +class UnspentCoinsInfo extends HiveObject { + UnspentCoinsInfo({ + this.walletId, + this.hash, + this.isFrozen, + this.isSending, + this.note}); + + static const typeId = 9; + static const boxName = 'Unspent'; + static const boxKey = 'unspentBoxKey'; + + @HiveField(0) + String walletId; + + @HiveField(1) + String hash; + + @HiveField(2) + bool isFrozen; + + @HiveField(3) + bool isSending; + + @HiveField(4) + String note; +} \ No newline at end of file diff --git a/lib/di.dart b/lib/di.dart index d887d8832..82ac29b6d 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/bitcoin/bitcoin_wallet_service.dart'; import 'package:cake_wallet/bitcoin/litecoin_wallet_service.dart'; +import 'package:cake_wallet/bitcoin/unspent_coins_info.dart'; import 'package:cake_wallet/core/backup_service.dart'; import 'package:cake_wallet/core/wallet_service.dart'; import 'package:cake_wallet/entities/biometric_auth.dart'; @@ -130,6 +131,7 @@ Box