From d3eb10a3fefd70493eb2b6fdd5dd94925f246b3f Mon Sep 17 00:00:00 2001 From: cassandras-lies <203535133+cassandras-lies@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:48:12 +0000 Subject: [PATCH 1/5] Fix IP and port serialization in Firo masternode transactions. --- lib/wallets/wallet/impl/firo_wallet.dart | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index bbb9f106a..c3b861ff7 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -999,9 +999,7 @@ class FiroWallet extends Bip39HDWallet final ipParts = ip .split('.') .map((e) => int.parse(e)) - .toList() - .reversed - .toList(); // network byte order + .toList(); if (ipParts.length != 4) { throw Exception("Invalid IP address: $ip"); } @@ -1017,11 +1015,12 @@ class FiroWallet extends Bip39HDWallet registrationTx.add(ipParts); // addr.port (2 bytes) - if (port < 0 || port > 65535) { + if (port < 1 || port > 65535) { throw Exception("Invalid port: $port"); } registrationTx.add( - (ByteData(2)..setInt16(0, port, Endian.little)).buffer.asUint8List(), + // network byte order + (ByteData(2)..setInt16(0, port, Endian.big)).buffer.asUint8List(), ); // keyIDOwner (20 bytes) From 824795ae83887481ee24f1c3b7cad88fc44990e6 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 26 Jan 2026 13:14:45 -0600 Subject: [PATCH 2/5] enable firo masternodes ui --- lib/pages/wallet_view/wallet_view.dart | 43 ++++++++++--------- .../sub_widgets/desktop_wallet_features.dart | 5 ++- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 8114224ad..74a129efd 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -94,6 +94,7 @@ import '../coin_control/coin_control_view.dart'; import '../epic_finalize_view/epic_finalize_view.dart'; import '../exchange_view/wallet_initiated_exchange_view.dart'; import '../finalize_view/finalize_view.dart'; +import '../masternodes/masternodes_home_view.dart'; import '../monkey/monkey_view.dart'; import '../namecoin_names/namecoin_names_home_view.dart'; import '../notification_views/notifications_view.dart'; @@ -1202,27 +1203,27 @@ class _WalletViewState extends ConsumerState { ); }, ), - // if (!viewOnly && wallet is FiroWallet) - // WalletNavigationBarItemData( - // label: "Masternodes", - // icon: SvgPicture.asset( - // Assets.svg.recycle, - // height: 20, - // width: 20, - // colorFilter: ColorFilter.mode( - // Theme.of( - // context, - // ).extension()!.bottomNavIconIcon, - // BlendMode.srcIn, - // ), - // ), - // onTap: () { - // Navigator.of(context).pushNamed( - // MasternodesHomeView.routeName, - // arguments: widget.walletId, - // ); - // }, - // ), + if (!viewOnly && wallet is FiroWallet) + WalletNavigationBarItemData( + label: "Masternodes", + icon: SvgPicture.asset( + Assets.svg.recycle, + height: 20, + width: 20, + colorFilter: ColorFilter.mode( + Theme.of( + context, + ).extension()!.bottomNavIconIcon, + BlendMode.srcIn, + ), + ), + onTap: () { + Navigator.of(context).pushNamed( + MasternodesHomeView.routeName, + arguments: widget.walletId, + ); + }, + ), if (wallet is NamecoinWallet) WalletNavigationBarItemData( label: "Domains", diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart index 052c08c2b..a9458bfd9 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart @@ -504,8 +504,9 @@ class _DesktopWalletFeaturesState extends ConsumerState { if (wallet is SignVerifyInterface && !isViewOnly) (WalletFeature.sign, Assets.svg.pencil, _onSignPressed), - // if ( !isViewOnly && wallet is FiroWallet) - // (WalletFeature.masternodes, Assets.svg.recycle, _onMasternodesPressed), + if (!isViewOnly && wallet is FiroWallet) + (WalletFeature.masternodes, Assets.svg.recycle, _onMasternodesPressed), + if (showCoinControl) ( WalletFeature.coinControl, From 9e3763a3f9baae71671a1118567f4b1f7a5d5416 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 27 Jan 2026 08:22:14 -0600 Subject: [PATCH 3/5] fix firo getAddressType to account for spark addresses --- lib/wallets/crypto_currency/coins/firo.dart | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/wallets/crypto_currency/coins/firo.dart b/lib/wallets/crypto_currency/coins/firo.dart index ac3ca1cac..583dc4b8d 100644 --- a/lib/wallets/crypto_currency/coins/firo.dart +++ b/lib/wallets/crypto_currency/coins/firo.dart @@ -197,7 +197,10 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface { } bool validateSparkAddress(String address) { - return SparkInterface.validateSparkAddress(address: address, isTestNet: network.isTestNet); + return SparkInterface.validateSparkAddress( + address: address, + isTestNet: network.isTestNet, + ); } bool isExchangeAddress(String address) { @@ -295,4 +298,12 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface { @override BigInt get defaultFeeRate => BigInt.from(1000); + + @override + AddressType? getAddressType(String address) { + if (validateSparkAddress(address)) { + return .spark; + } + return super.getAddressType(address); + } } From 34bb5b92c29a201d56abe89328c7c359168ca061 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 27 Jan 2026 09:11:49 -0600 Subject: [PATCH 4/5] add extra safeguards to catch unexpected wizardswap api responses and log them better --- .../exchange/wizard_swap/wizard_swap_api.dart | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/lib/services/exchange/wizard_swap/wizard_swap_api.dart b/lib/services/exchange/wizard_swap/wizard_swap_api.dart index 302b113f3..9e19e1ffa 100644 --- a/lib/services/exchange/wizard_swap/wizard_swap_api.dart +++ b/lib/services/exchange/wizard_swap/wizard_swap_api.dart @@ -77,21 +77,40 @@ abstract class WizardSwapApi { } } + static Map _decode(dynamic map) { + if (map is! Map) { + throw Exception( + "Expected a `Map`, but found a `${map.runtimeType}: $map", + ); + } + + try { + return Map.from(map); + } catch (_) { + Logging.instance.e("$map is NOT Map"); + rethrow; + } + } + static Future>> getCurrencies() async { final body = await _makeGetRequest(_getUri("/currency")); final data = jsonDecode(body); - return List>.from(data as List); + if (data is! List) { + throw Exception("$body is not a json list!"); + } + + return data.map(_decode).toList(); } /// [symbol] should be lowercase. Example: btc static Future> getCurrencyInfo(String symbol) async { final body = await _makeGetRequest(_getUri("/currency/$symbol")); - return Map.from(jsonDecode(body) as Map); + return _decode(jsonDecode(body)); } static Future getExchange(String id) async { final body = await _makeGetRequest(_getUri("/exchange/$id")); - return Map.from(jsonDecode(body) as Map); + return _decode(jsonDecode(body)); } static Future postEstimate( @@ -107,7 +126,7 @@ abstract class WizardSwapApi { "api_key": apiKey, }); - final map = Map.from(jsonDecode(body) as Map); + final map = _decode(jsonDecode(body)); // sometimes this json value will contain an error message lol... final amount = Decimal.tryParse(map["estimated_amount"].toString()); @@ -143,7 +162,7 @@ abstract class WizardSwapApi { if (refundExtraId != null) "refund_extra_id": refundExtraId, "api_key": apiKey, }); - return Map.from(jsonDecode(body) as Map); + return _decode(jsonDecode(body)); } } From 6e981ef07894dd28c5d005cf341744e07dfa2658 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 27 Jan 2026 12:35:16 -0600 Subject: [PATCH 5/5] add option to choose between spark and transparent addresses during swap when selecting choose from stack to fill in an address --- lib/pages/buy_view/buy_form.dart | 13 +- .../choose_address_from_stack_view.dart | 344 +++++++++++++ .../exchange_view/choose_from_stack_view.dart | 149 ------ .../exchange_step_views/step_2_view.dart | 45 +- .../subwidgets/desktop_step_2.dart | 457 +++++++++--------- .../desktop_choose_address_from_stack.dart | 426 ++++++++++++++++ .../subwidgets/desktop_choose_from_stack.dart | 312 ------------ lib/route_generator.dart | 6 +- .../sub_widgets/wallet_info_row_balance.dart | 14 +- 9 files changed, 1039 insertions(+), 727 deletions(-) create mode 100644 lib/pages/exchange_view/choose_address_from_stack_view.dart delete mode 100644 lib/pages/exchange_view/choose_from_stack_view.dart create mode 100644 lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_address_from_stack.dart delete mode 100644 lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart diff --git a/lib/pages/buy_view/buy_form.dart b/lib/pages/buy_view/buy_form.dart index 47911044d..93b64400d 100644 --- a/lib/pages/buy_view/buy_form.dart +++ b/lib/pages/buy_view/buy_form.dart @@ -57,7 +57,7 @@ import '../../widgets/stack_dialog.dart'; import '../../widgets/stack_text_field.dart'; import '../../widgets/textfield_icon_button.dart'; import '../address_book_views/address_book_view.dart'; -import '../exchange_view/choose_from_stack_view.dart'; +import '../exchange_view/choose_address_from_stack_view.dart'; import 'buy_quote_preview.dart'; import 'sub_widgets/crypto_selection_view.dart'; import 'sub_widgets/fiat_selection_view.dart'; @@ -1172,14 +1172,19 @@ class _BuyFormState extends ConsumerState { ); Navigator.of(context) .pushNamed( - ChooseFromStackView.routeName, + ChooseAddressFromStackView.routeName, arguments: coin, ) .then((value) async { - if (value is String) { + if (value + is ({ + String walletId, + String address, + String walletName, + })) { final wallet = ref .read(pWallets) - .getWallet(value); + .getWallet(value.walletId); // _toController.text = manager.walletName; // model.recipientAddress = diff --git a/lib/pages/exchange_view/choose_address_from_stack_view.dart b/lib/pages/exchange_view/choose_address_from_stack_view.dart new file mode 100644 index 000000000..c17c58b1a --- /dev/null +++ b/lib/pages/exchange_view/choose_address_from_stack_view.dart @@ -0,0 +1,344 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../../providers/providers.dart'; +import '../../themes/stack_colors.dart'; +import '../../utilities/assets.dart'; +import '../../utilities/constants.dart'; +import '../../utilities/show_loading.dart'; +import '../../utilities/text_styles.dart'; +import '../../utilities/util.dart'; +import '../../wallets/crypto_currency/crypto_currency.dart'; +import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; +import '../../widgets/background.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/rounded_white_container.dart'; +import '../../widgets/stack_dialog.dart'; +import '../../widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart'; +import '../../widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart'; + +class ChooseAddressFromStackView extends ConsumerStatefulWidget { + const ChooseAddressFromStackView({super.key, required this.coin}); + + final CryptoCurrency coin; + + static const String routeName = "/chooseFromStack"; + + @override + ConsumerState createState() => + _ChooseFromStackViewState(); +} + +class _ChooseFromStackViewState + extends ConsumerState { + late final CryptoCurrency coin; + + @override + void initState() { + coin = widget.coin; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final walletIds = ref + .watch(pWallets) + .wallets + .where((e) => e.info.coin == coin) + .map((e) => e.walletId) + .toList(); + + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + leading: const AppBarBackButton(), + title: Text( + "Choose your ${coin.ticker.toUpperCase()} wallet", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: walletIds.isEmpty + ? Column( + children: [ + RoundedWhiteContainer( + child: Center( + child: Text( + "No ${coin.ticker.toUpperCase()} wallets", + style: STextStyles.itemSubtitle(context), + ), + ), + ), + ], + ) + : ListView.builder( + itemCount: walletIds.length, + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.symmetric(vertical: 5.0), + child: _WalletAddressSelectCard( + walletId: walletIds[index], + ), + ), + ), + ), + ), + ), + ); + } +} + +class _WalletAddressSelectCard extends ConsumerStatefulWidget { + const _WalletAddressSelectCard({required this.walletId}); + + final String walletId; + + @override + ConsumerState<_WalletAddressSelectCard> createState() => + _WalletAddressSelectCardState(); +} + +class _WalletAddressSelectCardState + extends ConsumerState<_WalletAddressSelectCard> { + @override + Widget build(BuildContext context) { + final coin = ref.watch(pWalletCoin(widget.walletId)); + + if (coin is! Firo) { + return RawMaterialButton( + splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + padding: const EdgeInsets.all(0), + elevation: 0, + onPressed: () async { + final wallet = ref.read(pWallets).getWallet(widget.walletId); + + final data = ( + walletId: widget.walletId, + address: + (await wallet.getCurrentReceivingAddress())?.value ?? + wallet.info.cachedReceivingAddress, + walletName: wallet.info.name, + ); + + if (context.mounted) { + Navigator.of(context).pop(data); + } + }, + child: RoundedWhiteContainer( + child: Row( + children: [ + WalletInfoCoinIcon(coin: coin), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ref.watch(pWalletName(widget.walletId)), + style: STextStyles.titleBold12(context), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + WalletInfoRowBalance(walletId: widget.walletId), + ], + ), + ), + ], + ), + ), + ); + } + + return RoundedWhiteContainer( + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Row( + children: [ + WalletInfoCoinIcon(coin: coin), + const SizedBox(width: 12), + Expanded( + child: Text( + ref.watch(pWalletName(widget.walletId)), + style: STextStyles.titleBold12(context), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 10), + RawMaterialButton( + splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + padding: const EdgeInsets.all(0), + elevation: 0, + onPressed: () async { + Future _future() async { + final wallet = + ref.read(pWallets).getWallet(widget.walletId) + as SparkInterface; + final sparkAddress = await wallet + .getCurrentReceivingSparkAddress(); + if (sparkAddress != null) { + return sparkAddress.value; + } + + return (await wallet.generateNextSparkAddress( + saveToDB: true, + )).value; + } + + Exception? ex; + final sparkAddress = await showLoading( + context: context, + message: "Fetching Spark address", + rootNavigator: Util.isDesktop, + delay: const Duration(milliseconds: 1200), + whileFutureAlt: _future, + onException: (e) => ex = e, + ); + + if (context.mounted) { + if (ex != null) { + await showDialog( + context: context, + builder: (context) => StackOkDialog( + title: "Error", + message: ex + .toString() + .replaceFirst("Exception:", "") + .trim(), + ), + ); + } else { + Navigator.of(context).pop(( + walletId: widget.walletId, + address: sparkAddress, + walletName: + "${ref.read(pWalletName(widget.walletId))} (Spark)", + )); + } + } + }, + child: Row( + crossAxisAlignment: .center, + mainAxisAlignment: .spaceBetween, + children: [ + Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Text("Spark address", style: STextStyles.w500_12(context)), + const SizedBox(height: 2), + WalletInfoRowBalance( + walletId: widget.walletId, + balanceType: .private, + ), + ], + ), + SizedBox( + width: 25, + height: 25, + child: SvgPicture.asset( + Assets.svg.chevronRight, + colorFilter: ColorFilter.mode( + Theme.of(context).extension()!.textDark, + BlendMode.srcIn, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 8), + RawMaterialButton( + splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + padding: const EdgeInsets.all(0), + elevation: 0, + onPressed: () async { + final wallet = ref.read(pWallets).getWallet(widget.walletId); + + final data = ( + walletId: widget.walletId, + address: + (await wallet.getCurrentReceivingAddress())?.value ?? + wallet.info.cachedReceivingAddress, + walletName: "${wallet.info.name} (Transparent)", + ); + + if (context.mounted) { + Navigator.of(context).pop(data); + } + }, + + child: Row( + crossAxisAlignment: .center, + mainAxisAlignment: .spaceBetween, + children: [ + Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Text( + "Transparent address", + style: STextStyles.w500_12(context), + ), + const SizedBox(height: 2), + WalletInfoRowBalance( + walletId: widget.walletId, + balanceType: .public, + ), + ], + ), + SizedBox( + width: 25, + height: 25, + child: SvgPicture.asset( + Assets.svg.chevronRight, + colorFilter: ColorFilter.mode( + Theme.of(context).extension()!.textDark, + BlendMode.srcIn, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/exchange_view/choose_from_stack_view.dart b/lib/pages/exchange_view/choose_from_stack_view.dart deleted file mode 100644 index d4cc11dac..000000000 --- a/lib/pages/exchange_view/choose_from_stack_view.dart +++ /dev/null @@ -1,149 +0,0 @@ -/* - * This file is part of Stack Wallet. - * - * Copyright (c) 2023 Cypher Stack - * All Rights Reserved. - * The code is distributed under GPLv3 license, see LICENSE file for details. - * Generated by Cypher Stack on 2023-05-26 - * - */ - -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import '../../providers/providers.dart'; -import '../../themes/stack_colors.dart'; -import '../../utilities/constants.dart'; -import '../../utilities/text_styles.dart'; -import '../../wallets/crypto_currency/crypto_currency.dart'; -import '../../wallets/isar/providers/wallet_info_provider.dart'; -import '../../widgets/background.dart'; -import '../../widgets/custom_buttons/app_bar_icon_button.dart'; -import '../../widgets/rounded_white_container.dart'; -import '../../widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart'; -import '../../widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart'; - -class ChooseFromStackView extends ConsumerStatefulWidget { - const ChooseFromStackView({super.key, required this.coin}); - - final CryptoCurrency coin; - - static const String routeName = "/chooseFromStack"; - - @override - ConsumerState createState() => - _ChooseFromStackViewState(); -} - -class _ChooseFromStackViewState extends ConsumerState { - late final CryptoCurrency coin; - - @override - void initState() { - coin = widget.coin; - super.initState(); - } - - @override - Widget build(BuildContext context) { - final walletIds = - ref - .watch(pWallets) - .wallets - .where((e) => e.info.coin == coin) - .map((e) => e.walletId) - .toList(); - - return Background( - child: Scaffold( - backgroundColor: Theme.of(context).extension()!.background, - appBar: AppBar( - leading: const AppBarBackButton(), - title: Text( - "Choose your ${coin.ticker.toUpperCase()} wallet", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(16), - child: - walletIds.isEmpty - ? Column( - children: [ - RoundedWhiteContainer( - child: Center( - child: Text( - "No ${coin.ticker.toUpperCase()} wallets", - style: STextStyles.itemSubtitle(context), - ), - ), - ), - ], - ) - : ListView.builder( - itemCount: walletIds.length, - itemBuilder: (context, index) { - final walletId = walletIds[index]; - - return Padding( - padding: const EdgeInsets.symmetric(vertical: 5.0), - child: RawMaterialButton( - splashColor: - Theme.of( - context, - ).extension()!.highlight, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - padding: const EdgeInsets.all(0), - // color: Theme.of(context).extension()!.popupBG, - elevation: 0, - onPressed: () async { - if (mounted) { - Navigator.of(context).pop(walletId); - } - }, - child: RoundedWhiteContainer( - // color: Colors.transparent, - child: Row( - children: [ - WalletInfoCoinIcon(coin: coin), - const SizedBox(width: 12), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - ref.watch(pWalletName(walletId)), - style: STextStyles.titleBold12( - context, - ), - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 2), - WalletInfoRowBalance( - walletId: walletIds[index], - ), - ], - ), - ), - ], - ), - ), - ), - ); - }, - ), - ), - ), - ), - ); - } -} diff --git a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart index 3bead4106..1b2fa42c4 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart @@ -35,7 +35,7 @@ import '../../../widgets/stack_text_field.dart'; import '../../../widgets/textfield_icon_button.dart'; import '../../address_book_views/address_book_view.dart'; import '../../address_book_views/subviews/contact_popup.dart'; -import '../choose_from_stack_view.dart'; +import '../choose_address_from_stack_view.dart'; import '../sub_widgets/step_row.dart'; import 'step_3_view.dart'; @@ -299,24 +299,21 @@ class _Step2ViewState extends ConsumerState { Navigator.of(context) .pushNamed( - ChooseFromStackView.routeName, + ChooseAddressFromStackView + .routeName, arguments: coin, ) .then((value) async { - if (value is String) { - final wallet = ref - .read(pWallets) - .getWallet(value); - + if (value + is ({ + String walletId, + String address, + String walletName, + })) { _toController.text = - wallet.info.name; + value.walletName; model.recipientAddress = - (await wallet - .getCurrentReceivingAddress()) - ?.value ?? - wallet - .info - .cachedReceivingAddress; + value.address; setState(() { enableNext = @@ -571,21 +568,21 @@ class _Step2ViewState extends ConsumerState { Navigator.of(context) .pushNamed( - ChooseFromStackView.routeName, + ChooseAddressFromStackView + .routeName, arguments: coin, ) .then((value) async { - if (value is String) { - final wallet = ref - .read(pWallets) - .getWallet(value); - + if (value + is ({ + String walletId, + String address, + String walletName, + })) { _refundController.text = - wallet.info.name; + value.walletName; model.refundAddress = - (await wallet - .getCurrentReceivingAddress())! - .value; + value.address; } setState(() { enableNext = diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart index c7ba641ca..46038a58d 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart @@ -31,7 +31,7 @@ import '../../../../widgets/rounded_white_container.dart'; import '../../../../widgets/stack_text_field.dart'; import '../../../../widgets/textfield_icon_button.dart'; import '../../../my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart'; -import '../../subwidgets/desktop_choose_from_stack.dart'; +import '../../subwidgets/desktop_choose_address_from_stack.dart'; import '../step_scaffold.dart'; class DesktopStep2 extends ConsumerStatefulWidget { @@ -59,23 +59,21 @@ class _DesktopStep2State extends ConsumerState { void selectRecipientAddressFromStack() async { try { - final coin = - AppConfig.getCryptoCurrencyForTicker( - ref.read(desktopExchangeModelProvider)!.receiveTicker, - )!; + final coin = AppConfig.getCryptoCurrencyForTicker( + ref.read(desktopExchangeModelProvider)!.receiveTicker, + )!; final info = await showDialog?>( context: context, barrierColor: Colors.transparent, - builder: - (context) => DesktopDialog( - maxWidth: 720, - maxHeight: 670, - child: Padding( - padding: const EdgeInsets.all(32), - child: DesktopChooseFromStack(coin: coin), - ), - ), + builder: (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Padding( + padding: const EdgeInsets.all(32), + child: DesktopChooseAddressFromStack(coin: coin), + ), + ), ); if (info is Tuple2) { @@ -91,23 +89,21 @@ class _DesktopStep2State extends ConsumerState { void selectRefundAddressFromStack() async { try { - final coin = - AppConfig.getCryptoCurrencyForTicker( - ref.read(desktopExchangeModelProvider)!.sendTicker, - )!; + final coin = AppConfig.getCryptoCurrencyForTicker( + ref.read(desktopExchangeModelProvider)!.sendTicker, + )!; final info = await showDialog?>( context: context, barrierColor: Colors.transparent, - builder: - (context) => DesktopDialog( - maxWidth: 720, - maxHeight: 670, - child: Padding( - padding: const EdgeInsets.all(32), - child: DesktopChooseFromStack(coin: coin), - ), - ), + builder: (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Padding( + padding: const EdgeInsets.all(32), + child: DesktopChooseAddressFromStack(coin: coin), + ), + ), ); if (info is Tuple2) { _refundController.text = info.item1; @@ -127,30 +123,29 @@ class _DesktopStep2State extends ConsumerState { final entry = await showDialog( context: context, barrierColor: Colors.transparent, - builder: - (context) => DesktopDialog( - maxWidth: 720, - maxHeight: 670, - child: Column( - mainAxisSize: MainAxisSize.min, + builder: (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "Address book", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Address book", + style: STextStyles.desktopH3(context), + ), ), - Expanded(child: AddressBookAddressChooser(coin: coin)), + const DesktopDialogCloseButton(), ], ), - ), + Expanded(child: AddressBookAddressChooser(coin: coin)), + ], + ), + ), ); if (entry != null) { @@ -168,30 +163,29 @@ class _DesktopStep2State extends ConsumerState { final entry = await showDialog( context: context, barrierColor: Colors.transparent, - builder: - (context) => DesktopDialog( - maxWidth: 720, - maxHeight: 670, - child: Column( - mainAxisSize: MainAxisSize.min, + builder: (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "Address book", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Address book", + style: STextStyles.desktopH3(context), + ), ), - Expanded(child: AddressBookAddressChooser(coin: coin)), + const DesktopDialogCloseButton(), ], ), - ), + Expanded(child: AddressBookAddressChooser(coin: coin)), + ], + ), + ), ); if (entry != null) { @@ -234,12 +228,11 @@ class _DesktopStep2State extends ConsumerState { if (tuple != null) { if (ref.read(desktopExchangeModelProvider)!.receiveTicker.toLowerCase() == tuple.item2.ticker.toLowerCase()) { - _toController.text = - ref - .read(pWallets) - .getWallet(tuple.item1) - .info - .cachedReceivingAddress; + _toController.text = ref + .read(pWallets) + .getWallet(tuple.item1) + .info + .cachedReceivingAddress; WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(desktopExchangeModelProvider)!.recipientAddress = @@ -249,12 +242,11 @@ class _DesktopStep2State extends ConsumerState { if (doesRefundAddress && ref.read(desktopExchangeModelProvider)!.sendTicker.toUpperCase() == tuple.item2.ticker.toUpperCase()) { - _refundController.text = - ref - .read(pWallets) - .getWallet(tuple.item1) - .info - .cachedReceivingAddress; + _refundController.text = ref + .read(pWallets) + .getWallet(tuple.item1) + .info + .cachedReceivingAddress; WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(desktopExchangeModelProvider)!.refundAddress = _refundController.text; @@ -300,10 +292,9 @@ class _DesktopStep2State extends ConsumerState { Text( "Recipient Wallet", style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textFieldActiveSearchIconRight, + color: Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), ), if (AppConfig.isStackCoin( @@ -347,81 +338,82 @@ class _DesktopStep2State extends ConsumerState { _toController.text; widget.enableNextChanged.call(_next()); }, - decoration: standardInputDecoration( - "Enter the ${ref.watch(desktopExchangeModelProvider.select((value) => value!.receiveTicker.toUpperCase()))} payout address", - _toFocusNode, - context, - desktopMed: true, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: - _toController.text.isEmpty + decoration: + standardInputDecoration( + "Enter the ${ref.watch(desktopExchangeModelProvider.select((value) => value!.receiveTicker.toUpperCase()))} payout address", + _toFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _toController.text.isEmpty ? const EdgeInsets.only(right: 8) : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _toController.text.isNotEmpty - ? TextFieldIconButton( - key: const Key( - "sendViewClearAddressFieldButtonKey", - ), - onTap: () { - _toController.text = ""; - ref - .read(desktopExchangeModelProvider)! - .recipientAddress = _toController.text; - widget.enableNextChanged.call(_next()); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "sendViewPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = await clipboard - .getData(Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - final content = data.text!.trim(); - _toController.text = content; - ref - .read(desktopExchangeModelProvider)! - .recipientAddress = _toController.text; - widget.enableNextChanged.call(_next()); - } - }, - child: - _toController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_toController.text.isEmpty && - AppConfig.isStackCoin( - ref.watch( - desktopExchangeModelProvider.select( - (value) => value!.receiveTicker, - ), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _toController.text.isNotEmpty + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey", + ), + onTap: () { + _toController.text = ""; + ref + .read(desktopExchangeModelProvider)! + .recipientAddress = + _toController.text; + widget.enableNextChanged.call(_next()); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + final content = data.text!.trim(); + _toController.text = content; + ref + .read(desktopExchangeModelProvider)! + .recipientAddress = _toController + .text; + widget.enableNextChanged.call(_next()); + } + }, + child: _toController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_toController.text.isEmpty && + AppConfig.isStackCoin( + ref.watch( + desktopExchangeModelProvider.select( + (value) => value!.receiveTicker, + ), + ), + )) + TextFieldIconButton( + key: const Key("sendViewAddressBookButtonKey"), + onTap: selectRecipientFromAddressBook, + child: const AddressBookIcon(), ), - )) - TextFieldIconButton( - key: const Key("sendViewAddressBookButtonKey"), - onTap: selectRecipientFromAddressBook, - child: const AddressBookIcon(), - ), - ], + ], + ), + ), ), ), - ), - ), ), ), const SizedBox(height: 10), @@ -440,10 +432,9 @@ class _DesktopStep2State extends ConsumerState { Text( "Refund Wallet (required)", style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: - Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), ), if (AppConfig.isStackCoin( @@ -487,84 +478,87 @@ class _DesktopStep2State extends ConsumerState { _refundController.text; widget.enableNextChanged.call(_next()); }, - decoration: standardInputDecoration( - "Enter ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} refund address", - _refundFocusNode, - context, - desktopMed: true, - ).copyWith( - contentPadding: const EdgeInsets.only( - left: 16, - top: 6, - bottom: 8, - right: 5, - ), - suffixIcon: Padding( - padding: - _refundController.text.isEmpty + decoration: + standardInputDecoration( + "Enter ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} refund address", + _refundFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _refundController.text.isEmpty ? const EdgeInsets.only(right: 16) : const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _refundController.text.isNotEmpty - ? TextFieldIconButton( - key: const Key( - "sendViewClearAddressFieldButtonKey", - ), - onTap: () { - _refundController.text = ""; - ref - .read(desktopExchangeModelProvider)! - .refundAddress = _refundController.text; - - widget.enableNextChanged.call(_next()); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "sendViewPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = await clipboard - .getData(Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - final content = data.text!.trim(); - - _refundController.text = content; - ref - .read(desktopExchangeModelProvider)! - .refundAddress = _refundController.text; - - widget.enableNextChanged.call(_next()); - } - }, - child: - _refundController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), - if (_refundController.text.isEmpty && - AppConfig.isStackCoin( - ref.watch( - desktopExchangeModelProvider.select( - (value) => value!.sendTicker, - ), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _refundController.text.isNotEmpty + ? TextFieldIconButton( + key: const Key( + "sendViewClearAddressFieldButtonKey", + ), + onTap: () { + _refundController.text = ""; + ref + .read(desktopExchangeModelProvider)! + .refundAddress = _refundController + .text; + + widget.enableNextChanged.call(_next()); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? data = + await clipboard.getData( + Clipboard.kTextPlain, + ); + if (data?.text != null && + data!.text!.isNotEmpty) { + final content = data.text!.trim(); + + _refundController.text = content; + ref + .read(desktopExchangeModelProvider)! + .refundAddress = _refundController + .text; + + widget.enableNextChanged.call(_next()); + } + }, + child: _refundController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_refundController.text.isEmpty && + AppConfig.isStackCoin( + ref.watch( + desktopExchangeModelProvider.select( + (value) => value!.sendTicker, + ), + ), + )) + TextFieldIconButton( + key: const Key("sendViewAddressBookButtonKey"), + onTap: selectRefundFromAddressBook, + child: const AddressBookIcon(), ), - )) - TextFieldIconButton( - key: const Key("sendViewAddressBookButtonKey"), - onTap: selectRefundFromAddressBook, - child: const AddressBookIcon(), - ), - ], + ], + ), + ), ), ), - ), - ), ), ), if (doesRefundAddress) const SizedBox(height: 10), @@ -572,7 +566,8 @@ class _DesktopStep2State extends ConsumerState { RoundedWhiteContainer( borderColor: Theme.of(context).extension()!.background, child: Text( - "In case something goes wrong during the exchange, we might need a refund address so we can return your coins back to you.", + "In case something goes wrong during the exchange, we might need " + "a refund address so we can return your coins back to you.", style: STextStyles.desktopTextExtraExtraSmall(context), ), ), diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_address_from_stack.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_address_from_stack.dart new file mode 100644 index 000000000..8eaf94991 --- /dev/null +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_address_from_stack.dart @@ -0,0 +1,426 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:tuple/tuple.dart'; + +import '../../../app_config.dart'; +import '../../../providers/providers.dart'; +import '../../../providers/wallet/public_private_balance_state_provider.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/amount/amount_formatter.dart'; +import '../../../utilities/assets.dart'; +import '../../../utilities/constants.dart'; +import '../../../utilities/show_loading.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/crypto_currency/crypto_currency.dart'; +import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; +import '../../../widgets/custom_buttons/blue_text_button.dart'; +import '../../../widgets/desktop/secondary_button.dart'; +import '../../../widgets/icon_widgets/x_icon.dart'; +import '../../../widgets/rounded_white_container.dart'; +import '../../../widgets/stack_dialog.dart'; +import '../../../widgets/stack_text_field.dart'; +import '../../../widgets/textfield_icon_button.dart'; +import '../../../widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart'; + +class DesktopChooseAddressFromStack extends ConsumerStatefulWidget { + const DesktopChooseAddressFromStack({super.key, required this.coin}); + + final CryptoCurrency coin; + + @override + ConsumerState createState() => + _DesktopChooseFromStackState(); +} + +class _DesktopChooseFromStackState + extends ConsumerState { + late final TextEditingController _searchController; + late final FocusNode searchFieldFocusNode; + + String _searchTerm = ""; + + List filter(List walletIds, String searchTerm) { + if (searchTerm.isEmpty) { + return walletIds; + } + + final List result = []; + for (final walletId in walletIds) { + final name = ref.read(pWalletName(walletId)); + + if (name.toLowerCase().contains(searchTerm.toLowerCase())) { + result.add(walletId); + } + } + + return result; + } + + @override + void initState() { + searchFieldFocusNode = FocusNode(); + _searchController = TextEditingController(); + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + searchFieldFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Choose from ${AppConfig.prefix}", + style: STextStyles.desktopH3(context), + ), + const SizedBox(height: 28), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: false, + enableSuggestions: false, + controller: _searchController, + focusNode: searchFieldFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of( + context, + ).extension()!.textFieldActiveText, + height: 1.8, + ), + decoration: + standardInputDecoration( + "Search", + searchFieldFocusNode, + context, + desktopMed: true, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 18, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 20, + height: 20, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox(height: 16), + Flexible( + child: Builder( + builder: (context) { + final wallets = ref + .watch(pWallets) + .wallets + .where((e) => e.info.coin == widget.coin); + + if (wallets.isEmpty) { + return Column( + children: [ + RoundedWhiteContainer( + borderColor: Theme.of( + context, + ).extension()!.background, + child: Center( + child: Text( + "No ${widget.coin.ticker.toUpperCase()} wallets", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + ), + ), + ], + ); + } + + List walletIds = wallets.map((e) => e.walletId).toList(); + + walletIds = filter(walletIds, _searchTerm); + + return ListView.separated( + primary: false, + itemCount: walletIds.length, + separatorBuilder: (_, __) => const SizedBox(height: 5), + itemBuilder: (context, index) => + _WalletRow(walletId: walletIds[index]), + ); + }, + ), + ), + const SizedBox(height: 20), + Row( + children: [ + const Spacer(), + const SizedBox(width: 16), + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of(context).pop, + ), + ), + ], + ), + ], + ); + } +} + +class _BalanceDisplay extends ConsumerWidget { + const _BalanceDisplay({super.key, required this.walletId, this.balanceType}); + + final String walletId; + final BalanceType? balanceType; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final coin = ref.watch(pWalletCoin(walletId)); + final total = balanceType == BalanceType.public + ? ref.watch(pWalletBalance(walletId)).total + : balanceType == BalanceType.private + ? ref.watch(pWalletBalanceSecondary(walletId)).total + + ref.watch(pWalletBalanceTertiary(walletId)).total + : ref.watch(pWalletBalance(walletId)).total + + ref.watch(pWalletBalanceSecondary(walletId)).total + + ref.watch(pWalletBalanceTertiary(walletId)).total; + + return Text( + ref.watch(pAmountFormatter(coin)).format(total), + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension()!.textSubtitle1, + ), + textAlign: TextAlign.right, + ); + } +} + +class _WalletRow extends ConsumerWidget { + const _WalletRow({super.key, required this.walletId}); + + final String walletId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final coin = ref.watch(pWalletCoin(walletId)); + + if (coin is! Firo) { + return RoundedWhiteContainer( + borderColor: Theme.of(context).extension()!.background, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + child: Row( + children: [ + Row( + children: [ + WalletInfoCoinIcon(coin: coin), + const SizedBox(width: 12), + Text( + ref.watch(pWalletName(walletId)), + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of( + context, + ).extension()!.textDark, + ), + ), + ], + ), + const Spacer(), + _BalanceDisplay(walletId: walletId), + const SizedBox(width: 80), + CustomTextButton( + text: "Select wallet", + onTap: () async { + final wallet = ref.read(pWallets).getWallet(walletId); + final address = + (await wallet.getCurrentReceivingAddress())?.value ?? + wallet.info.cachedReceivingAddress; + + if (context.mounted) { + Navigator.of(context).pop(Tuple2(wallet.info.name, address)); + } + }, + ), + ], + ), + ); + } + + return RoundedWhiteContainer( + borderColor: Theme.of(context).extension()!.background, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + Row( + children: [ + WalletInfoCoinIcon(coin: coin, size: 32), + const SizedBox(width: 12), + Text( + ref.watch(pWalletName(walletId)), + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + ), + ], + ), + const SizedBox(height: 10), + Row( + children: [ + const SizedBox( + width: 12 + 32, // space + size of WalletInfoCoinIcon + ), + Text( + "Spark", + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + ), + const Spacer(), + _BalanceDisplay(walletId: walletId, balanceType: .private), + const SizedBox(width: 80), + CustomTextButton( + text: "Select wallet", + onTap: () async { + Future _future() async { + final wallet = + ref.read(pWallets).getWallet(walletId) + as SparkInterface; + + final sparkAddress = await wallet + .getCurrentReceivingSparkAddress(); + if (sparkAddress != null) { + return sparkAddress.value; + } + + return (await wallet.generateNextSparkAddress( + saveToDB: true, + )).value; + } + + Exception? ex; + final sparkAddress = await showLoading( + context: context, + message: "Fetching Spark address", + rootNavigator: Util.isDesktop, + delay: const Duration(milliseconds: 1200), + whileFutureAlt: _future, + onException: (e) => ex = e, + ); + + if (context.mounted) { + if (ex != null) { + await showDialog( + context: context, + builder: (context) => StackOkDialog( + title: "Error", + message: ex + .toString() + .replaceFirst("Exception:", "") + .trim(), + maxWidth: 400, + desktopPopRootNavigator: true, + ), + ); + } else { + Navigator.of(context).pop( + sparkAddress == null + ? null + : Tuple2( + "${ref.read(pWalletName(walletId))} (Spark)", + sparkAddress, + ), + ); + } + } + }, + ), + ], + ), + const SizedBox(height: 10), + Row( + children: [ + const SizedBox( + width: 12 + 32, // space + size of WalletInfoCoinIcon + ), + Text( + "Transparent", + style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + ), + const Spacer(), + _BalanceDisplay(walletId: walletId, balanceType: .public), + const SizedBox(width: 80), + CustomTextButton( + text: "Select wallet", + onTap: () async { + final wallet = ref.read(pWallets).getWallet(walletId); + final address = + (await wallet.getCurrentReceivingAddress())?.value ?? + wallet.info.cachedReceivingAddress; + + if (context.mounted) { + Navigator.of( + context, + ).pop(Tuple2("${wallet.info.name} (Transparent)", address)); + } + }, + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart deleted file mode 100644 index 1dec08ff8..000000000 --- a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart +++ /dev/null @@ -1,312 +0,0 @@ -/* - * This file is part of Stack Wallet. - * - * Copyright (c) 2023 Cypher Stack - * All Rights Reserved. - * The code is distributed under GPLv3 license, see LICENSE file for details. - * Generated by Cypher Stack on 2023-05-26 - * - */ - -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:tuple/tuple.dart'; - -import '../../../app_config.dart'; -import '../../../providers/providers.dart'; -import '../../../themes/stack_colors.dart'; -import '../../../utilities/amount/amount.dart'; -import '../../../utilities/amount/amount_formatter.dart'; -import '../../../utilities/assets.dart'; -import '../../../utilities/constants.dart'; -import '../../../utilities/text_styles.dart'; -import '../../../wallets/crypto_currency/crypto_currency.dart'; -import '../../../wallets/isar/providers/wallet_info_provider.dart'; -import '../../../widgets/custom_buttons/blue_text_button.dart'; -import '../../../widgets/desktop/secondary_button.dart'; -import '../../../widgets/icon_widgets/x_icon.dart'; -import '../../../widgets/rounded_white_container.dart'; -import '../../../widgets/stack_text_field.dart'; -import '../../../widgets/textfield_icon_button.dart'; -import '../../../widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart'; - -class DesktopChooseFromStack extends ConsumerStatefulWidget { - const DesktopChooseFromStack({ - super.key, - required this.coin, - }); - - final CryptoCurrency coin; - - @override - ConsumerState createState() => - _DesktopChooseFromStackState(); -} - -class _DesktopChooseFromStackState - extends ConsumerState { - late final TextEditingController _searchController; - late final FocusNode searchFieldFocusNode; - - String _searchTerm = ""; - - List filter(List walletIds, String searchTerm) { - if (searchTerm.isEmpty) { - return walletIds; - } - - final List result = []; - for (final walletId in walletIds) { - final name = ref.read(pWalletName(walletId)); - - if (name.toLowerCase().contains(searchTerm.toLowerCase())) { - result.add(walletId); - } - } - - return result; - } - - @override - void initState() { - searchFieldFocusNode = FocusNode(); - _searchController = TextEditingController(); - super.initState(); - } - - @override - void dispose() { - _searchController.dispose(); - searchFieldFocusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Choose from ${AppConfig.prefix}", - style: STextStyles.desktopH3(context), - ), - const SizedBox( - height: 28, - ), - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: false, - enableSuggestions: false, - controller: _searchController, - focusNode: searchFieldFocusNode, - onChanged: (value) { - setState(() { - _searchTerm = value; - }); - }, - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - height: 1.8, - ), - decoration: standardInputDecoration( - "Search", - searchFieldFocusNode, - context, - desktopMed: true, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 18, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 20, - height: 20, - ), - ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - _searchTerm = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - const SizedBox( - height: 16, - ), - Flexible( - child: Builder( - builder: (context) { - final wallets = ref - .watch(pWallets) - .wallets - .where((e) => e.info.coin == widget.coin); - - if (wallets.isEmpty) { - return Column( - children: [ - RoundedWhiteContainer( - borderColor: Theme.of(context) - .extension()! - .background, - child: Center( - child: Text( - "No ${widget.coin.ticker.toUpperCase()} wallets", - style: - STextStyles.desktopTextExtraExtraSmall(context), - ), - ), - ), - ], - ); - } - - List walletIds = wallets.map((e) => e.walletId).toList(); - - walletIds = filter(walletIds, _searchTerm); - - return ListView.separated( - primary: false, - itemCount: walletIds.length, - separatorBuilder: (_, __) => const SizedBox( - height: 5, - ), - itemBuilder: (context, index) { - final wallet = ref.watch( - pWallets - .select((value) => value.getWallet(walletIds[index])), - ); - - return RoundedWhiteContainer( - borderColor: - Theme.of(context).extension()!.background, - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 14, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Row( - children: [ - WalletInfoCoinIcon(coin: widget.coin), - const SizedBox( - width: 12, - ), - Text( - wallet.info.name, - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), - ), - ], - ), - const Spacer(), - _BalanceDisplay( - walletId: walletIds[index], - ), - const SizedBox( - width: 80, - ), - CustomTextButton( - text: "Select wallet", - onTap: () async { - final address = - (await wallet.getCurrentReceivingAddress()) - ?.value ?? - wallet.info.cachedReceivingAddress; - - if (mounted) { - Navigator.of(context).pop( - Tuple2( - wallet.info.name, - address, - ), - ); - } - }, - ), - ], - ), - ); - }, - ); - }, - ), - ), - const SizedBox( - height: 20, - ), - Row( - children: [ - const Spacer(), - const SizedBox( - width: 16, - ), - Expanded( - child: SecondaryButton( - label: "Cancel", - buttonHeight: ButtonHeight.l, - onPressed: Navigator.of(context).pop, - ), - ), - ], - ), - ], - ); - } -} - -class _BalanceDisplay extends ConsumerWidget { - const _BalanceDisplay({ - super.key, - required this.walletId, - }); - - final String walletId; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final coin = ref.watch(pWalletCoin(walletId)); - Amount total = ref.watch(pWalletBalance(walletId)).total; - if (coin is Firo) { - total += ref.watch(pWalletBalanceSecondary(walletId)).total; - total += ref.watch(pWalletBalanceTertiary(walletId)).total; - } - - return Text( - ref.watch(pAmountFormatter(coin)).format(total), - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle1, - ), - textAlign: TextAlign.right, - ); - } -} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index e6a24ec7f..21c20a074 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -64,7 +64,7 @@ import 'pages/churning/churning_view.dart'; import 'pages/coin_control/coin_control_view.dart'; import 'pages/coin_control/utxo_details_view.dart'; import 'pages/epic_finalize_view/epic_finalize_view.dart'; -import 'pages/exchange_view/choose_from_stack_view.dart'; +import 'pages/exchange_view/choose_address_from_stack_view.dart'; import 'pages/exchange_view/edit_trade_note_view.dart'; import 'pages/exchange_view/exchange_step_views/step_1_view.dart'; import 'pages/exchange_view/exchange_step_views/step_2_view.dart'; @@ -2104,11 +2104,11 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case ChooseFromStackView.routeName: + case ChooseAddressFromStackView.routeName: if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ChooseFromStackView(coin: args), + builder: (_) => ChooseAddressFromStackView(coin: args), settings: RouteSettings(name: settings.name), ); } diff --git a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart index ef81c10bd..a1ec77214 100644 --- a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart +++ b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart @@ -13,6 +13,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../db/isar/main_db.dart'; import '../../../models/isar/models/contract.dart'; +import '../../../providers/wallet/public_private_balance_state_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/amount/amount_formatter.dart'; @@ -28,10 +29,12 @@ class WalletInfoRowBalance extends ConsumerWidget { super.key, required this.walletId, this.contractAddress, + this.balanceType, }); final String walletId; final String? contractAddress; + final BalanceType? balanceType; @override Widget build(BuildContext context, WidgetRef ref) { @@ -41,10 +44,13 @@ class WalletInfoRowBalance extends ConsumerWidget { Contract? contract; if (contractAddress == null) { - totalBalance = - info.cachedBalance.total + - info.cachedBalanceSecondary.total + - info.cachedBalanceTertiary.total; + totalBalance = balanceType == BalanceType.private + ? info.cachedBalanceSecondary.total + info.cachedBalanceTertiary.total + : balanceType == BalanceType.public + ? info.cachedBalance.total + : info.cachedBalance.total + + info.cachedBalanceSecondary.total + + info.cachedBalanceTertiary.total; contract = null; } else {