diff --git a/.gitignore b/.gitignore index a36824135..e2b5c5a6a 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,8 @@ test/services/coins/particl/particl_wallet_test_parameters.dart coverage scripts/**/build /lib/external_api_keys.dart + +libcw_monero.dll +libcw_wownero.dll +libepic_cash_wallet.dll +libmobileliblelantus.dll diff --git a/crypto_plugins/flutter_libepiccash b/crypto_plugins/flutter_libepiccash index 9a150d8cd..0309140a9 160000 --- a/crypto_plugins/flutter_libepiccash +++ b/crypto_plugins/flutter_libepiccash @@ -1 +1 @@ -Subproject commit 9a150d8cd2c3625424b0059e6b7306f3659fdbe0 +Subproject commit 0309140a95a51388df0effcc39ff0a25b2752b29 diff --git a/crypto_plugins/flutter_liblelantus b/crypto_plugins/flutter_liblelantus index db928e6f1..6864d7c0d 160000 --- a/crypto_plugins/flutter_liblelantus +++ b/crypto_plugins/flutter_liblelantus @@ -1 +1 @@ -Subproject commit db928e6f11844138ce87de2b8c1abe3305ec589e +Subproject commit 6864d7c0d4fa68c371e3f0c067afd50b0d59cc9b diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index de29931da..66eaa2f3c 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit de29931dacc9aefaf42a9ca139a8754a42adc40d +Subproject commit 66eaa2f3c7133f1dbf0b1fc950e7a9e3cc611185 diff --git a/dockerfile.linux b/dockerfile.linux index 0853741d9..4a3867008 100644 --- a/dockerfile.linux +++ b/dockerfile.linux @@ -2,10 +2,10 @@ FROM ubuntu:20.04 as base COPY . /stack_wallet WORKDIR /stack_wallet/scripts/linux RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y git=1:2.25.1-1ubuntu3.6 make=4.2.1-1.2 curl=7.68.0-1ubuntu2.14 cargo=0.62.0ubuntu0libgit2-0ubuntu0.20.04.1 \ - file=1:5.38-4 ca-certificates=20211016~20.04.1 cmake=3.16.3-1ubuntu1.20.04.1 cmake-data=3.16.3-1ubuntu1.20.04.1 g++=4:9.3.0-1ubuntu2 libgmp-dev=2:6.2.0+dfsg-4ubuntu0.1 libssl-dev=1.1.1f-1ubuntu2.16 libclang-dev=1:10.0-50~exp1 \ - unzip=6.0-25ubuntu1.1 python3=3.8.2-0ubuntu2 pkg-config=0.29.1-0ubuntu4 libglib2.0-dev=2.64.6-1~ubuntu20.04.4 libgcrypt20-dev=1.8.5-5ubuntu1.1 gettext-base=0.19.8.1-10build1 libgirepository1.0-dev=1.64.1-1~ubuntu20.04.1 \ - valac=0.48.6-0ubuntu1 xsltproc=1.1.34-4ubuntu0.20.04.1 docbook-xsl=1.79.1+dfsg-2 python3-pip=20.0.2-5ubuntu1.6 ninja-build=1.10.0-1build1 clang=1:10.0-50~exp1 libgtk-3-dev=3.24.20-0ubuntu1.1 \ - libunbound-dev=1.9.4-2ubuntu1.4 libzmq3-dev=4.3.2-2ubuntu1 libtool=2.4.6-14 autoconf=2.69-11.1 automake=1:1.16.1-4ubuntu6 bison=2:3.5.1+dfsg-1 \ + file=1:5.38-4 ca-certificates=20211016ubuntu0.20.04.1 cmake=3.16.3-1ubuntu1.20.04.1 cmake-data=3.16.3-1ubuntu1.20.04.1 g++=4:9.3.0-1ubuntu2 libgmp-dev=2:6.2.0+dfsg-4ubuntu0.1 libssl-dev=1.1.1f-1ubuntu2.16 \ + libclang-dev=1:10.0-50~exp1 unzip=6.0-25ubuntu1.1 python3=3.8.2-0ubuntu2 pkg-config=0.29.1-0ubuntu4 libglib2.0-dev=2.64.6-1~ubuntu20.04.4 libgcrypt20-dev=1.8.5-5ubuntu1.1 gettext-base=0.19.8.1-10build1 \ + libgirepository1.0-dev=1.64.1-1~ubuntu20.04.1 valac=0.48.6-0ubuntu1 xsltproc=1.1.34-4ubuntu0.20.04.1 docbook-xsl=1.79.1+dfsg-2 python3-pip=20.0.2-5ubuntu1.6 ninja-build=1.10.0-1build1 clang=1:10.0-50~exp1 \ + libgtk-3-dev=3.24.20-0ubuntu1.1 libunbound-dev=1.9.4-2ubuntu1.4 libzmq3-dev=4.3.2-2ubuntu1 libtool=2.4.6-14 autoconf=2.69-11.1 automake=1:1.16.1-4ubuntu6 bison=2:3.5.1+dfsg-1 \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && pip3 install --upgrade meson==0.64.1 markdown==3.4.1 markupsafe==2.1.1 jinja2==3.1.2 pygments==2.13.0 toml==0.10.2 typogrify==2.0.7 tomli==2.0.1 && cd .. && ./prebuild.sh && cd linux && ./build_all.sh diff --git a/lib/models/exchange/exchange_form_state.dart b/lib/models/exchange/exchange_form_state.dart index 5efa446a2..86be9b34a 100644 --- a/lib/models/exchange/exchange_form_state.dart +++ b/lib/models/exchange/exchange_form_state.dart @@ -7,7 +7,7 @@ import 'package:stackwallet/models/exchange/response_objects/fixed_rate_market.d import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/exchange.dart'; -import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart'; +// import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart'; import 'package:stackwallet/utilities/logger.dart'; class ExchangeFormState extends ChangeNotifier { @@ -53,8 +53,8 @@ class ExchangeFormState extends ChangeNotifier { return _from?.ticker; case ExchangeRateType.fixed: switch (exchange?.name) { - case SimpleSwapExchange.exchangeName: - return _from?.ticker; + // case SimpleSwapExchange.exchangeName: + // return _from?.ticker; case ChangeNowExchange.exchangeName: return market?.from; default: @@ -69,8 +69,8 @@ class ExchangeFormState extends ChangeNotifier { return _to?.ticker; case ExchangeRateType.fixed: switch (exchange?.name) { - case SimpleSwapExchange.exchangeName: - return _to?.ticker; + // case SimpleSwapExchange.exchangeName: + // return _to?.ticker; case ChangeNowExchange.exchangeName: return market?.to; default: @@ -296,9 +296,9 @@ class ExchangeFormState extends ChangeNotifier { } Future updateRanges({required bool shouldNotifyListeners}) async { - if (exchange?.name == SimpleSwapExchange.exchangeName) { - reversed = false; - } + // if (exchange?.name == SimpleSwapExchange.exchangeName) { + // reversed = false; + // } final _fromTicker = reversed ? toTicker : fromTicker; final _toTicker = reversed ? fromTicker : toTicker; if (_fromTicker == null || _toTicker == null) { @@ -340,9 +340,9 @@ class ExchangeFormState extends ChangeNotifier { required bool shouldNotifyListeners, required bool reversed, }) async { - if (exchange?.name == SimpleSwapExchange.exchangeName) { - reversed = false; - } + // if (exchange?.name == SimpleSwapExchange.exchangeName) { + // reversed = false; + // } final amount = reversed ? toAmount : fromAmount; if (fromTicker == null || toTicker == null || diff --git a/lib/models/paymint/transactions_model.dart b/lib/models/paymint/transactions_model.dart index 06a21e16a..6eba877c4 100644 --- a/lib/models/paymint/transactions_model.dart +++ b/lib/models/paymint/transactions_model.dart @@ -380,8 +380,9 @@ class Output { factory Output.fromJson(Map json) { // TODO determine if any of this code is needed. - // Particl has different tx types that need to be detected and handled here - if (json.containsKey('scriptPubKey') as bool) { + try { + // Particl has different tx types that need to be detected and handled here + // if (json.containsKey('scriptPubKey') as bool) { // output is transparent final address = json["scriptPubKey"]["addresses"] == null ? json['scriptPubKey']['type'] as String @@ -397,17 +398,30 @@ class Output { .toBigInt() .toInt(), ); - } /* else if (json.containsKey('ct_fee') as bool) { - // or type: data - // output is blinded (CT) - } else if (json.containsKey('rangeproof') as bool) { - // or valueCommitment or type: anon - // output is private (RingCT) - } */ - else { - // TODO detect staking - // TODO handle CT, RingCT, and staking accordingly - // print("transaction not supported: ${json}"); + // } /* else if (json.containsKey('ct_fee') as bool) { + // // or type: data + // // output is blinded (CT) + // } else if (json.containsKey('rangeproof') as bool) { + // // or valueCommitment or type: anon + // // output is private (RingCT) + // } */ + // else { + // // TODO detect staking + // // TODO handle CT, RingCT, and staking accordingly + // // print("transaction not supported: ${json}"); + // return Output( + // // Return output object with null values; allows wallet history to be built + // scriptpubkey: "", + // scriptpubkeyAsm: "", + // scriptpubkeyType: "", + // scriptpubkeyAddress: "", + // value: (Decimal.parse(0.toString()) * + // Decimal.fromInt(Constants.satsPerCoin(Coin + // .firo))) // dirty hack but we need 8 decimal places here to keep consistent data structure + // .toBigInt() + // .toInt()); + // } + } catch (s, e) { return Output( // Return output object with null values; allows wallet history to be built scriptpubkey: "", diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index b9b04cec8..a0190cfef 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_text.dart'; @@ -37,11 +39,19 @@ class _AddWalletViewState extends State { final List coins = [...Coin.values]; + final bool isDesktop = Util.isDesktop; + @override void initState() { _searchFieldController = TextEditingController(); _searchFocusNode = FocusNode(); coins.remove(Coin.firoTestNet); + if (isDesktop) { + coins.remove(Coin.wownero); + if (Platform.isWindows) { + coins.remove(Coin.monero); + } + } super.initState(); } @@ -56,7 +66,7 @@ class _AddWalletViewState extends State { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - if (Util.isDesktop) { + if (isDesktop) { return DesktopScaffold( appBar: const DesktopAppBar( isCompactHeight: false, diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart index 38181b9e1..935b5f231 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart @@ -33,9 +33,9 @@ class SearchableCoinList extends ConsumerWidget { _coins.remove(Coin.firoTestNet); // Kidgloves for Wownero on desktop - if(isDesktop) { - _coins.remove(Coin.wownero); - } + // if(isDesktop) { + // _coins.remove(Coin.wownero); + // } return _coins; } diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart index b159fb9aa..97dc91361 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart @@ -19,6 +19,7 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; @@ -105,8 +106,25 @@ class _NewWalletRecoveryPhraseWarningViewState ) ], ), - body: Padding( - padding: EdgeInsets.all(isDesktop ? 0 : 16), + body: ConditionalParent( + condition: !isDesktop, + builder: (child) => LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ), + ); + }, + ), child: Column( crossAxisAlignment: isDesktop ? CrossAxisAlignment.center @@ -315,9 +333,11 @@ class _NewWalletRecoveryPhraseWarningViewState const SizedBox( width: 20, ), - Text( - "Do not show them to anyone.", - style: STextStyles.navBarTitle(context), + Expanded( + child: Text( + "Do not show them to anyone.", + style: STextStyles.navBarTitle(context), + ), ), ], ), @@ -327,6 +347,10 @@ class _NewWalletRecoveryPhraseWarningViewState ), ), if (!isDesktop) const Spacer(), + if (!isDesktop) + const SizedBox( + height: 16, + ), if (isDesktop) const SizedBox( height: 32, diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart index dac2af028..6b03d06fb 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart @@ -23,8 +23,6 @@ import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; -import 'package:stackwallet/widgets/rounded_date_picker/flutter_rounded_date_picker_widget.dart' - as datePicker; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:tuple/tuple.dart'; @@ -154,10 +152,46 @@ class _RestoreOptionsViewState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 125)); } - final date = await datePicker.showRoundedDatePicker( + final date = await showRoundedDatePicker( context: context, initialDate: DateTime.now(), - height: height * 0.5, + height: height / 3.0, + theme: ThemeData( + primarySwatch: Util.createMaterialColor(fetchedColor), + ), + //TODO pick a better initial date + // 2007 chosen as that is just before bitcoin launched + firstDate: DateTime(2007), + lastDate: DateTime.now(), + borderRadius: Constants.size.circularBorderRadius * 2, + + textPositiveButton: "SELECT", + + styleDatePicker: _buildDatePickerStyle(), + styleYearPicker: _buildYearPickerStyle(), + ); + if (date != null) { + _restoreFromDate = date; + _dateController.text = Format.formatDate(date); + } + } + + Future chooseDesktopDate() async { + final height = MediaQuery.of(context).size.height; + final fetchedColor = + Theme.of(context).extension()!.accentColorDark; + // check and hide keyboard + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 125)); + } + + final now = DateTime.now(); + + final date = await showRoundedDatePicker( + context: context, + initialDate: DateTime.now(), + height: height / 3.0, theme: ThemeData( primarySwatch: Util.createMaterialColor(fetchedColor), ), @@ -283,15 +317,22 @@ class _RestoreOptionsViewState extends ConsumerState { (coin == Coin.wownero && ref.watch(mnemonicWordCountStateProvider.state).state == 25)) - - // if (!isDesktop) - RestoreFromDatePicker( - onTap: chooseDate, - controller: _dateController, - ), - - // if (isDesktop) - // // TODO desktop date picker + if (!isDesktop) + RestoreFromDatePicker( + onTap: chooseDate, + controller: _dateController, + ), + if (coin == Coin.monero || + coin == Coin.epicCash || + (coin == Coin.wownero && + ref.watch(mnemonicWordCountStateProvider.state).state == + 25)) + if (isDesktop) + // TODO desktop date picker + RestoreFromDatePicker( + onTap: chooseDesktopDate, + controller: _dateController, + ), if (coin == Coin.monero || coin == Coin.epicCash || (coin == Coin.wownero && @@ -412,7 +453,6 @@ class _RestoreOptionsViewState extends ConsumerState { isDesktop: isDesktop, onPressed: _nextEnabled ? nextPressed : null, ), - if (isDesktop) const Spacer( flex: 15, diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index 7a21a6f72..bb2628192 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -736,6 +736,8 @@ class _RestoreWalletViewState extends ConsumerState { child: Column( children: [ TextFormField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, textCapitalization: TextCapitalization.none, key: Key( @@ -831,6 +833,8 @@ class _RestoreWalletViewState extends ConsumerState { child: Column( children: [ TextFormField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, textCapitalization: TextCapitalization.none, key: Key( @@ -954,6 +958,8 @@ class _RestoreWalletViewState extends ConsumerState { padding: const EdgeInsets.symmetric(vertical: 4), child: TextFormField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, textCapitalization: TextCapitalization.none, key: Key("restoreMnemonicFormField_$i"), diff --git a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart index 0ce1fac26..e8a680678 100644 --- a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart @@ -161,7 +161,7 @@ class _VerifyRecoveryPhraseViewState result.insert(random.nextInt(wordsToShow), chosenWord); //todo: this prints sensitive info - // debugPrint("Mnemonic game correct word: $chosenWord"); + debugPrint("Mnemonic game correct word: $chosenWord"); return Tuple2(result, chosenWord); } diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index f584e16e5..032afacdb 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -20,7 +20,7 @@ import 'package:stackwallet/pages/exchange_view/sub_widgets/rate_type_toggle.dar import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; -import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart'; +// import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -69,17 +69,21 @@ class _ExchangeFormState extends ConsumerState { bool _swapLock = false; void sendFieldOnChanged(String value) async { - final newFromAmount = Decimal.tryParse(value); + if (_sendFocusNode.hasFocus) { + final newFromAmount = Decimal.tryParse(value); - ref.read(exchangeFormStateProvider).fromAmount = - newFromAmount ?? Decimal.zero; + await ref + .read(exchangeFormStateProvider) + .setFromAmountAndCalculateToAmount( + newFromAmount ?? Decimal.zero, true); - if (newFromAmount == null) { - _receiveController.text = - ref.read(prefsChangeNotifierProvider).exchangeRateType == - ExchangeRateType.estimated - ? "-" - : ""; + if (newFromAmount == null) { + _receiveController.text = + ref.read(prefsChangeNotifierProvider).exchangeRateType == + ExchangeRateType.estimated + ? "-" + : ""; + } } } @@ -101,11 +105,11 @@ class _ExchangeFormState extends ConsumerState { currencies = ref.read(availableChangeNowCurrenciesProvider).currencies; break; - case SimpleSwapExchange.exchangeName: - currencies = ref - .read(availableSimpleswapCurrenciesProvider) - .floatingRateCurrencies; - break; + // case SimpleSwapExchange.exchangeName: + // currencies = ref + // .read(availableSimpleswapCurrenciesProvider) + // .floatingRateCurrencies; + // break; default: currencies = []; } @@ -116,6 +120,31 @@ class _ExchangeFormState extends ConsumerState { fromTicker: fromTicker, onSelected: (from) => ref.read(exchangeFormStateProvider).updateFrom(from, true)); + + unawaited( + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => WillPopScope( + onWillPop: () async => false, + child: Container( + color: Theme.of(context) + .extension()! + .overlay + .withOpacity(0.6), + child: const CustomLoadingOverlay( + message: "Updating exchange rate", + eventBus: null, + ), + ), + ), + ), + ); + + await Future.delayed(const Duration(milliseconds: 300)); + + Navigator.of(context, rootNavigator: true).pop(); + } else { final toTicker = ref.read(exchangeFormStateProvider).toTicker ?? ""; final fromTicker = ref.read(exchangeFormStateProvider).fromTicker ?? ""; @@ -170,17 +199,17 @@ class _ExchangeFormState extends ConsumerState { }, ); break; - case SimpleSwapExchange.exchangeName: - await _showFloatingRateSelectionSheet( - currencies: ref - .read(availableSimpleswapCurrenciesProvider) - .fixedRateCurrencies, - excludedTicker: - ref.read(exchangeFormStateProvider).toTicker ?? "-", - fromTicker: fromTicker, - onSelected: (from) => - ref.read(exchangeFormStateProvider).updateFrom(from, true)); - break; + // case SimpleSwapExchange.exchangeName: + // await _showFloatingRateSelectionSheet( + // currencies: ref + // .read(availableSimpleswapCurrenciesProvider) + // .fixedRateCurrencies, + // excludedTicker: + // ref.read(exchangeFormStateProvider).toTicker ?? "-", + // fromTicker: fromTicker, + // onSelected: (from) => + // ref.read(exchangeFormStateProvider).updateFrom(from, true)); + // break; default: // TODO show error? } @@ -204,11 +233,11 @@ class _ExchangeFormState extends ConsumerState { currencies = ref.read(availableChangeNowCurrenciesProvider).currencies; break; - case SimpleSwapExchange.exchangeName: - currencies = ref - .read(availableSimpleswapCurrenciesProvider) - .floatingRateCurrencies; - break; + // case SimpleSwapExchange.exchangeName: + // currencies = ref + // .read(availableSimpleswapCurrenciesProvider) + // .floatingRateCurrencies; + // break; default: currencies = []; } @@ -272,17 +301,17 @@ class _ExchangeFormState extends ConsumerState { }, ); break; - case SimpleSwapExchange.exchangeName: - await _showFloatingRateSelectionSheet( - currencies: ref - .read(availableSimpleswapCurrenciesProvider) - .fixedRateCurrencies, - excludedTicker: - ref.read(exchangeFormStateProvider).fromTicker ?? "", - fromTicker: ref.read(exchangeFormStateProvider).fromTicker ?? "", - onSelected: (to) => - ref.read(exchangeFormStateProvider).updateTo(to, true)); - break; + // case SimpleSwapExchange.exchangeName: + // await _showFloatingRateSelectionSheet( + // currencies: ref + // .read(availableSimpleswapCurrenciesProvider) + // .fixedRateCurrencies, + // excludedTicker: + // ref.read(exchangeFormStateProvider).fromTicker ?? "", + // fromTicker: ref.read(exchangeFormStateProvider).fromTicker ?? "", + // onSelected: (to) => + // ref.read(exchangeFormStateProvider).updateTo(to, true)); + // break; default: // TODO show error? } @@ -374,12 +403,12 @@ class _ExchangeFormState extends ConsumerState { case ChangeNowExchange.exchangeName: allPairs = ref.read(availableChangeNowCurrenciesProvider).pairs; break; - case SimpleSwapExchange.exchangeName: - allPairs = ref.read(exchangeFormStateProvider).exchangeType == - ExchangeRateType.fixed - ? ref.read(availableSimpleswapCurrenciesProvider).fixedRatePairs - : ref.read(availableSimpleswapCurrenciesProvider).floatingRatePairs; - break; + // case SimpleSwapExchange.exchangeName: + // allPairs = ref.read(exchangeFormStateProvider).exchangeType == + // ExchangeRateType.fixed + // ? ref.read(availableSimpleswapCurrenciesProvider).fixedRatePairs + // : ref.read(availableSimpleswapCurrenciesProvider).floatingRatePairs; + // break; default: allPairs = []; } @@ -486,18 +515,18 @@ class _ExchangeFormState extends ConsumerState { .currencies .where((e) => e.ticker.toUpperCase() == ticker.toUpperCase()); break; - case SimpleSwapExchange.exchangeName: - possibleCurrencies = [ - ...ref - .read(availableSimpleswapCurrenciesProvider) - .fixedRateCurrencies - .where((e) => e.ticker.toUpperCase() == ticker.toUpperCase()), - ...ref - .read(availableSimpleswapCurrenciesProvider) - .floatingRateCurrencies - .where((e) => e.ticker.toUpperCase() == ticker.toUpperCase()), - ]; - break; + // case SimpleSwapExchange.exchangeName: + // possibleCurrencies = [ + // ...ref + // .read(availableSimpleswapCurrenciesProvider) + // .fixedRateCurrencies + // .where((e) => e.ticker.toUpperCase() == ticker.toUpperCase()), + // ...ref + // .read(availableSimpleswapCurrenciesProvider) + // .floatingRateCurrencies + // .where((e) => e.ticker.toUpperCase() == ticker.toUpperCase()), + // ]; + // break; default: possibleCurrencies = []; } @@ -655,12 +684,12 @@ class _ExchangeFormState extends ConsumerState { .pairs .where((e) => e.to == toTicker && e.from == fromTicker); break; - case SimpleSwapExchange.exchangeName: - available = ref - .read(availableSimpleswapCurrenciesProvider) - .floatingRatePairs - .where((e) => e.to == toTicker && e.from == fromTicker); - break; + // case SimpleSwapExchange.exchangeName: + // available = ref + // .read(availableSimpleswapCurrenciesProvider) + // .floatingRatePairs + // .where((e) => e.to == toTicker && e.from == fromTicker); + // break; default: available = []; } @@ -675,13 +704,13 @@ class _ExchangeFormState extends ConsumerState { .where( (e) => e.ticker == fromTicker || e.ticker == toTicker); break; - case SimpleSwapExchange.exchangeName: - availableCurrencies = ref - .read(availableSimpleswapCurrenciesProvider) - .floatingRateCurrencies - .where( - (e) => e.ticker == fromTicker || e.ticker == toTicker); - break; + // case SimpleSwapExchange.exchangeName: + // availableCurrencies = ref + // .read(availableSimpleswapCurrenciesProvider) + // .floatingRateCurrencies + // .where( + // (e) => e.ticker == fromTicker || e.ticker == toTicker); + // break; default: availableCurrencies = []; } @@ -763,47 +792,47 @@ class _ExchangeFormState extends ConsumerState { Navigator.of(context, rootNavigator: isDesktop).pop(); } return; - case SimpleSwapExchange.exchangeName: - final available = ref - .read(availableSimpleswapCurrenciesProvider) - .floatingRatePairs - .where((e) => e.to == toTicker && e.from == fromTicker); - if (available.isNotEmpty) { - final availableCurrencies = ref - .read(availableSimpleswapCurrenciesProvider) - .fixedRateCurrencies - .where( - (e) => e.ticker == fromTicker || e.ticker == toTicker); - if (availableCurrencies.length > 1) { - final from = availableCurrencies - .firstWhere((e) => e.ticker == fromTicker); - final to = availableCurrencies - .firstWhere((e) => e.ticker == toTicker); - - final newFromAmount = Decimal.tryParse(_sendController.text); - ref.read(exchangeFormStateProvider).fromAmount = - newFromAmount ?? Decimal.zero; - if (newFromAmount == null) { - _receiveController.text = ""; - } - - await ref.read(exchangeFormStateProvider).updateTo(to, false); - await ref - .read(exchangeFormStateProvider) - .updateFrom(from, true); - - _receiveController.text = - ref.read(exchangeFormStateProvider).toAmountString.isEmpty - ? "-" - : ref.read(exchangeFormStateProvider).toAmountString; - if (mounted) { - Navigator.of(context, rootNavigator: isDesktop).pop(); - } - return; - } - } - - break; + // case SimpleSwapExchange.exchangeName: + // final available = ref + // .read(availableSimpleswapCurrenciesProvider) + // .floatingRatePairs + // .where((e) => e.to == toTicker && e.from == fromTicker); + // if (available.isNotEmpty) { + // final availableCurrencies = ref + // .read(availableSimpleswapCurrenciesProvider) + // .fixedRateCurrencies + // .where( + // (e) => e.ticker == fromTicker || e.ticker == toTicker); + // if (availableCurrencies.length > 1) { + // final from = availableCurrencies + // .firstWhere((e) => e.ticker == fromTicker); + // final to = availableCurrencies + // .firstWhere((e) => e.ticker == toTicker); + // + // final newFromAmount = Decimal.tryParse(_sendController.text); + // ref.read(exchangeFormStateProvider).fromAmount = + // newFromAmount ?? Decimal.zero; + // if (newFromAmount == null) { + // _receiveController.text = ""; + // } + // + // await ref.read(exchangeFormStateProvider).updateTo(to, false); + // await ref + // .read(exchangeFormStateProvider) + // .updateFrom(from, true); + // + // _receiveController.text = + // ref.read(exchangeFormStateProvider).toAmountString.isEmpty + // ? "-" + // : ref.read(exchangeFormStateProvider).toAmountString; + // if (mounted) { + // Navigator.of(context, rootNavigator: isDesktop).pop(); + // } + // return; + // } + // } + // + // break; default: // } @@ -844,12 +873,12 @@ class _ExchangeFormState extends ConsumerState { .pairs .where((e) => e.to == toTicker && e.from == fromTicker); break; - case SimpleSwapExchange.exchangeName: - availableFloatingPairs = ref - .read(availableSimpleswapCurrenciesProvider) - .floatingRatePairs - .where((e) => e.to == toTicker && e.from == fromTicker); - break; + // case SimpleSwapExchange.exchangeName: + // availableFloatingPairs = ref + // .read(availableSimpleswapCurrenciesProvider) + // .floatingRatePairs + // .where((e) => e.to == toTicker && e.from == fromTicker); + // break; default: availableFloatingPairs = []; } @@ -1117,38 +1146,43 @@ class _ExchangeFormState extends ConsumerState { : ref.read(exchangeFormStateProvider).toAmountString; } - _sendFocusNode.addListener(() async { - if (!_sendFocusNode.hasFocus) { - final newFromAmount = Decimal.tryParse(_sendController.text); - await ref - .read(exchangeFormStateProvider) - .setFromAmountAndCalculateToAmount( - newFromAmount ?? Decimal.zero, true); - - if (newFromAmount == null) { - _receiveController.text = - ref.read(prefsChangeNotifierProvider).exchangeRateType == - ExchangeRateType.estimated - ? "-" - : ""; - } - } - }); - _receiveFocusNode.addListener(() async { - if (!_receiveFocusNode.hasFocus) { - final newToAmount = Decimal.tryParse(_receiveController.text); - if (ref.read(prefsChangeNotifierProvider).exchangeRateType != - ExchangeRateType.estimated) { - await ref - .read(exchangeFormStateProvider) - .setToAmountAndCalculateFromAmount( - newToAmount ?? Decimal.zero, true); - } - if (newToAmount == null) { - _sendController.text = ""; - } - } - }); + // _sendFocusNode.addListener(() async { + // if (!_sendFocusNode.hasFocus) { + // final newFromAmount = Decimal.tryParse(_sendController.text); + // await ref + // .read(exchangeFormStateProvider) + // .setFromAmountAndCalculateToAmount( + // newFromAmount ?? Decimal.zero, true); + // + // debugPrint("SendFocusNode has fired"); + // + // if (newFromAmount == null) { + // _receiveController.text = + // ref.read(prefsChangeNotifierProvider).exchangeRateType == + // ExchangeRateType.estimated + // ? "-" + // : ""; + // } + // } + // }); + // + // _receiveFocusNode.addListener(() async { + // if (!_receiveFocusNode.hasFocus) { + // final newToAmount = Decimal.tryParse(_receiveController.text); + // if (ref.read(prefsChangeNotifierProvider).exchangeRateType != + // ExchangeRateType.estimated) { + // await ref + // .read(exchangeFormStateProvider) + // .setToAmountAndCalculateFromAmount( + // newToAmount ?? Decimal.zero, true); + // + // debugPrint("ReceiveFocusNode has fired"); + // } + // if (newToAmount == null) { + // _sendController.text = ""; + // } + // } + // }); super.initState(); } @@ -1164,6 +1198,7 @@ class _ExchangeFormState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); + // provider for simpleswap; not called rn ref.listen(currentExchangeNameStateProvider, (previous, next) { ref.read(exchangeFormStateProvider).exchange = ref.read(exchangeProvider); }); @@ -1176,12 +1211,9 @@ class _ExchangeFormState extends ConsumerState { exchangeFormStateProvider.select((value) => value.toAmountString), (previous, String next) { if (!_receiveFocusNode.hasFocus) { - _receiveController.text = isEstimated && - ref.watch(exchangeProvider).name == - SimpleSwapExchange.exchangeName && - next.isEmpty - ? "-" - : next; + // ref.watch(exchangeProvider).name == + // SimpleSwapExchange.exchangeName && + _receiveController.text = isEstimated && next.isEmpty ? "-" : next; //todo: check if print needed // debugPrint("RECEIVE AMOUNT LISTENER ACTIVATED"); if (_swapLock) { @@ -1334,10 +1366,11 @@ class _ExchangeFormState extends ConsumerState { ticker: ref.watch( exchangeFormStateProvider.select((value) => value.toTicker)), readOnly: ref.watch(prefsChangeNotifierProvider - .select((value) => value.exchangeRateType)) == - ExchangeRateType.estimated || - ref.watch(exchangeProvider).name == - SimpleSwapExchange.exchangeName, + .select((value) => value.exchangeRateType)) == + ExchangeRateType.estimated, + // || + // ref.watch(exchangeProvider).name == + // SimpleSwapExchange.exchangeName, ), if (ref .watch( @@ -1359,13 +1392,15 @@ class _ExchangeFormState extends ConsumerState { onChanged: onRateTypeChanged, ), ), - if (ref.read(exchangeFormStateProvider).fromAmount != null && - ref.read(exchangeFormStateProvider).fromAmount != Decimal.zero) + // these reads should be watch + if (ref.watch(exchangeFormStateProvider).fromAmount != null && + ref.watch(exchangeFormStateProvider).fromAmount != Decimal.zero) SizedBox( height: isDesktop ? 20 : 12, ), - if (ref.read(exchangeFormStateProvider).fromAmount != null && - ref.read(exchangeFormStateProvider).fromAmount != Decimal.zero) + // these reads should be watch + if (ref.watch(exchangeFormStateProvider).fromAmount != null && + ref.watch(exchangeFormStateProvider).fromAmount != Decimal.zero) ExchangeProviderOptions( from: ref.watch(exchangeFormStateProvider).fromTicker, to: ref.watch(exchangeFormStateProvider).toTicker, diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart index ebaf68066..42ffc398a 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart @@ -7,7 +7,6 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/exchange.dart'; import 'package:stackwallet/services/exchange/exchange_response.dart'; -import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -78,35 +77,43 @@ class ExchangeProviderOptions extends ConsumerWidget { SizedBox( width: 20, height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: ChangeNowExchange.exchangeName, - groupValue: ref - .watch(currentExchangeNameStateProvider.state) - .state, - onChanged: (value) { - if (value is String) { - ref - .read(currentExchangeNameStateProvider.state) - .state = value; - ref.read(exchangeFormStateProvider).exchange = - Exchange.fromName(ref - .read(currentExchangeNameStateProvider - .state) - .state); - } - }, + child: Padding( + padding: + EdgeInsets.only(top: isDesktop ? 20.0 : 15.0), + child: Radio( + activeColor: Theme.of(context) + .extension()! + .radioButtonIconEnabled, + value: ChangeNowExchange.exchangeName, + groupValue: ref + .watch(currentExchangeNameStateProvider.state) + .state, + onChanged: (value) { + if (value is String) { + ref + .read( + currentExchangeNameStateProvider.state) + .state = value; + ref.read(exchangeFormStateProvider).exchange = + Exchange.fromName(ref + .read(currentExchangeNameStateProvider + .state) + .state); + } + }, + ), ), ), const SizedBox( width: 14, ), - SvgPicture.asset( - Assets.exchange.changeNow, - width: isDesktop ? 32 : 24, - height: isDesktop ? 32 : 24, + Padding( + padding: const EdgeInsets.only(top: 5.0), + child: SvgPicture.asset( + Assets.exchange.changeNow, + width: isDesktop ? 32 : 24, + height: isDesktop ? 32 : 24, + ), ), const SizedBox( width: 10, @@ -250,207 +257,207 @@ class ExchangeProviderOptions extends ConsumerWidget { height: 1, color: Theme.of(context).extension()!.background, ), - if (!isDesktop) - const SizedBox( - height: 16, - ), - ConditionalParent( - condition: isDesktop, - builder: (child) => MouseRegion( - cursor: SystemMouseCursors.click, - child: child, - ), - child: GestureDetector( - onTap: () { - if (ref.read(currentExchangeNameStateProvider.state).state != - SimpleSwapExchange.exchangeName) { - ref.read(currentExchangeNameStateProvider.state).state = - SimpleSwapExchange.exchangeName; - ref.read(exchangeFormStateProvider).exchange = - Exchange.fromName(ref - .read(currentExchangeNameStateProvider.state) - .state); - } - }, - child: Container( - color: Colors.transparent, - child: Padding( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 20, - height: 20, - child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, - value: SimpleSwapExchange.exchangeName, - groupValue: ref - .watch(currentExchangeNameStateProvider.state) - .state, - onChanged: (value) { - if (value is String) { - ref - .read(currentExchangeNameStateProvider.state) - .state = value; - ref.read(exchangeFormStateProvider).exchange = - Exchange.fromName(ref - .read(currentExchangeNameStateProvider - .state) - .state); - } - }, - ), - ), - const SizedBox( - width: 14, - ), - SvgPicture.asset( - Assets.exchange.simpleSwap, - width: isDesktop ? 32 : 24, - height: isDesktop ? 32 : 24, - ), - const SizedBox( - width: 10, - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - SimpleSwapExchange.exchangeName, - style: STextStyles.titleBold12(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark2, - ), - ), - if (from != null && - to != null && - toAmount != null && - toAmount! > Decimal.zero && - fromAmount != null && - fromAmount! > Decimal.zero) - FutureBuilder( - future: SimpleSwapExchange().getEstimate( - from!, - to!, - // reversed ? toAmount! : fromAmount!, - fromAmount!, - fixedRate, - // reversed, - false, - ), - builder: (context, - AsyncSnapshot> - snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - final estimate = snapshot.data?.value; - if (estimate != null) { - Decimal rate = (estimate.estimatedAmount / - fromAmount!) - .toDecimal( - scaleOnInfinitePrecision: 12); - - Coin coin; - try { - coin = - coinFromTickerCaseInsensitive(to!); - } catch (_) { - coin = Coin.bitcoin; - } - return Text( - "1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed( - value: rate, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select( - (value) => value.locale), - ), - decimalPlaces: - Constants.decimalPlacesForCoin( - coin), - )} ${to!.toUpperCase()}", - style: - STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } else { - Logging.instance.log( - "$runtimeType failed to fetch rate for SimpleSwap: ${snapshot.data}", - level: LogLevel.Warning, - ); - return Text( - "Failed to fetch rate", - style: - STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading", - "Loading.", - "Loading..", - "Loading...", - ], - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ); - } - }, - ), - // if (!(from != null && - // to != null && - // (reversed - // ? toAmount != null && toAmount! > Decimal.zero - // : fromAmount != null && - // fromAmount! > Decimal.zero))) - if (!(from != null && - to != null && - toAmount != null && - toAmount! > Decimal.zero && - fromAmount != null && - fromAmount! > Decimal.zero)) - Text( - "n/a", - style: STextStyles.itemSubtitle12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ), - ], - ), - ), - ], - ), - ), - ), - ), - ), + // if (!isDesktop) + // const SizedBox( + // height: 16, + // ), + // ConditionalParent( + // condition: isDesktop, + // builder: (child) => MouseRegion( + // cursor: SystemMouseCursors.click, + // child: child, + // ), + // child: GestureDetector( + // onTap: () { + // if (ref.read(currentExchangeNameStateProvider.state).state != + // SimpleSwapExchange.exchangeName) { + // // ref.read(currentExchangeNameStateProvider.state).state = + // // SimpleSwapExchange.exchangeName; + // ref.read(exchangeFormStateProvider).exchange = + // Exchange.fromName(ref + // .read(currentExchangeNameStateProvider.state) + // .state); + // } + // }, + // child: Container( + // color: Colors.transparent, + // child: Padding( + // padding: isDesktop + // ? const EdgeInsets.all(16) + // : const EdgeInsets.all(0), + // child: Row( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // SizedBox( + // width: 20, + // height: 20, + // child: Radio( + // activeColor: Theme.of(context) + // .extension()! + // .radioButtonIconEnabled, + // value: SimpleSwapExchange.exchangeName, + // groupValue: ref + // .watch(currentExchangeNameStateProvider.state) + // .state, + // onChanged: (value) { + // if (value is String) { + // ref + // .read(currentExchangeNameStateProvider.state) + // .state = value; + // ref.read(exchangeFormStateProvider).exchange = + // Exchange.fromName(ref + // .read(currentExchangeNameStateProvider + // .state) + // .state); + // } + // }, + // ), + // ), + // const SizedBox( + // width: 14, + // ), + // // SvgPicture.asset( + // // Assets.exchange.simpleSwap, + // // width: isDesktop ? 32 : 24, + // // height: isDesktop ? 32 : 24, + // // ), + // // const SizedBox( + // // width: 10, + // // ), + // // Expanded( + // // child: Column( + // // mainAxisAlignment: MainAxisAlignment.start, + // // mainAxisSize: MainAxisSize.min, + // // crossAxisAlignment: CrossAxisAlignment.start, + // // children: [ + // // Text( + // // SimpleSwapExchange.exchangeName, + // // style: STextStyles.titleBold12(context).copyWith( + // // color: Theme.of(context) + // // .extension()! + // // .textDark2, + // // ), + // // ), + // // if (from != null && + // // to != null && + // // toAmount != null && + // // toAmount! > Decimal.zero && + // // fromAmount != null && + // // fromAmount! > Decimal.zero) + // // FutureBuilder( + // // future: SimpleSwapExchange().getEstimate( + // // from!, + // // to!, + // // // reversed ? toAmount! : fromAmount!, + // // fromAmount!, + // // fixedRate, + // // // reversed, + // // false, + // // ), + // // builder: (context, + // // AsyncSnapshot> + // // snapshot) { + // // if (snapshot.connectionState == + // // ConnectionState.done && + // // snapshot.hasData) { + // // final estimate = snapshot.data?.value; + // // if (estimate != null) { + // // Decimal rate = (estimate.estimatedAmount / + // // fromAmount!) + // // .toDecimal( + // // scaleOnInfinitePrecision: 12); + // // + // // Coin coin; + // // try { + // // coin = + // // coinFromTickerCaseInsensitive(to!); + // // } catch (_) { + // // coin = Coin.bitcoin; + // // } + // // return Text( + // // "1 ${from!.toUpperCase()} ~ ${Format.localizedStringAsFixed( + // // value: rate, + // // locale: ref.watch( + // // localeServiceChangeNotifierProvider + // // .select( + // // (value) => value.locale), + // // ), + // // decimalPlaces: + // // Constants.decimalPlacesForCoin( + // // coin), + // // )} ${to!.toUpperCase()}", + // // style: + // // STextStyles.itemSubtitle12(context) + // // .copyWith( + // // color: Theme.of(context) + // // .extension()! + // // .textSubtitle1, + // // ), + // // ); + // // } else { + // // Logging.instance.log( + // // "$runtimeType failed to fetch rate for SimpleSwap: ${snapshot.data}", + // // level: LogLevel.Warning, + // // ); + // // return Text( + // // "Failed to fetch rate", + // // style: + // // STextStyles.itemSubtitle12(context) + // // .copyWith( + // // color: Theme.of(context) + // // .extension()! + // // .textSubtitle1, + // // ), + // // ); + // // } + // // } else { + // // return AnimatedText( + // // stringsToLoopThrough: const [ + // // "Loading", + // // "Loading.", + // // "Loading..", + // // "Loading...", + // // ], + // // style: STextStyles.itemSubtitle12(context) + // // .copyWith( + // // color: Theme.of(context) + // // .extension()! + // // .textSubtitle1, + // // ), + // // ); + // // } + // // }, + // // ), + // // // if (!(from != null && + // // // to != null && + // // // (reversed + // // // ? toAmount != null && toAmount! > Decimal.zero + // // // : fromAmount != null && + // // // fromAmount! > Decimal.zero))) + // // if (!(from != null && + // // to != null && + // // toAmount != null && + // // toAmount! > Decimal.zero && + // // fromAmount != null && + // // fromAmount! > Decimal.zero)) + // // Text( + // // "n/a", + // // style: STextStyles.itemSubtitle12(context) + // // .copyWith( + // // color: Theme.of(context) + // // .extension()! + // // .textSubtitle1, + // // ), + // // ), + // // ], + // // ), + // // ), + // ], + // ), + // ), + // ), + // ), + // ), ], ), ); diff --git a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart index 27bc7b151..3c61681f0 100644 --- a/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart +++ b/lib/pages/receive_view/generate_receiving_uri_qr_code_view.dart @@ -409,8 +409,9 @@ class _GenerateUriQrCodeViewState extends State { height: 1.8, ) : STextStyles.field(context), - keyboardType: - const TextInputType.numberWithOptions(decimal: true), + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions(decimal: true), onChanged: (_) => setState(() {}), decoration: standardInputDecoration( "Amount", diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index b129c4467..e34b9d8ad 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -1111,10 +1111,12 @@ class _SendViewState extends ConsumerState { const Key("amountInputFieldCryptoTextFieldKey"), controller: cryptoAmountController, focusNode: _cryptoFocus, - keyboardType: const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.right, inputFormatters: [ // regex to validate a crypto amount with 8 decimal places @@ -1168,11 +1170,12 @@ class _SendViewState extends ConsumerState { const Key("amountInputFieldFiatTextFieldKey"), controller: baseAmountController, focusNode: _baseFocus, - keyboardType: - const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.right, inputFormatters: [ // regex to validate a fiat amount with 2 decimal places diff --git a/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart b/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart index 33f21759e..442ad5d2a 100644 --- a/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart +++ b/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart @@ -20,7 +20,6 @@ import 'package:stackwallet/providers/global/debug_service_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:stackwallet/utilities/text_styles.dart'; diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart index 92b712111..f27f57312 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart @@ -391,7 +391,8 @@ class _EpiBoxInfoFormState extends ConsumerState { enableSuggestions: Util.isDesktop ? false : true, controller: portController, decoration: const InputDecoration(hintText: "Port"), - keyboardType: const TextInputType.numberWithOptions(), + keyboardType: + Util.isDesktop ? null : const TextInputType.numberWithOptions(), ), const SizedBox( height: 8, diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart index f740eee12..f953a975a 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart @@ -57,7 +57,7 @@ class DeleteWalletWarningView extends ConsumerWidget { .extension()! .warningBackground, child: Text( - "You are going to permanently delete you wallet.\n\nIf you delete your wallet, the only way you can have access to your funds is by using your backup key.\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet.\n\nPLEASE SAVE YOUR BACKUP KEY.", + "You are going to permanently delete your wallet.\n\nIf you delete your wallet, the only way you can have access to your funds is by using your backup key.\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet.\n\nPLEASE SAVE YOUR BACKUP KEY.", style: STextStyles.baseXS(context).copyWith( color: Theme.of(context) .extension()! diff --git a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart index 7e1b53cbb..d0e013d83 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart @@ -739,10 +739,12 @@ class _TransactionSearchViewState controller: _amountTextEditingController, focusNode: amountTextFieldFocusNode, onChanged: (_) => setState(() {}), - keyboardType: const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), inputFormatters: [ // regex to validate a crypto amount with 8 decimal places TextInputFormatter.withFunction((oldValue, newValue) => diff --git a/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart b/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart index a0c561e3c..073ea73b4 100644 --- a/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart +++ b/lib/pages_desktop_specific/my_stack_view/coin_wallets_table.dart @@ -60,6 +60,14 @@ class CoinWalletsTable extends ConsumerWidget { ref.read(currentWalletIdProvider.state).state = walletIds[i]; + final manager = ref + .read(walletsChangeNotifierProvider) + .getManager(walletIds[i]); + if (manager.coin == Coin.monero || + manager.coin == Coin.wownero) { + await manager.initializeExisting(); + } + await Navigator.of(context).pushNamed( DesktopWalletView.routeName, arguments: walletIds[i], diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart index d7bfefb1f..1277c5c0f 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/sub_widgets/contact_list_item.dart @@ -42,6 +42,9 @@ class _ContactListItemState extends ConsumerState { final contact = ref.watch(addressBookServiceProvider .select((value) => value.getContactById(contactId))); + // hack fix until we use a proper database (not Hive) + int i = 0; + return RoundedWhiteContainer( padding: const EdgeInsets.all(0), borderColor: Theme.of(context).extension()!.background, @@ -70,7 +73,8 @@ class _ContactListItemState extends ConsumerState { filterByCoin != null ? e.coin == filterByCoin! : true) .map( (e) => Column( - key: Key("contactAddress_${e.address}_${e.label}_key"), + key: Key( + "contactAddress_${e.address}_${e.label}_${++i}_key"), mainAxisSize: MainAxisSize.min, children: [ Container( diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart index febb9f44d..e30436dc0 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart @@ -66,7 +66,7 @@ class _DesktopAttentionDeleteWallet child: Padding( padding: const EdgeInsets.all(10.0), child: Text( - "You are going to permanently delete you wallet.\n\nIf you delete your wallet, " + "You are going to permanently delete your wallet.\n\nIf you delete your wallet, " "the only way you can have access to your funds is by using your backup key." "\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet." "\n\nPLEASE SAVE YOUR BACKUP KEY.", @@ -74,7 +74,7 @@ class _DesktopAttentionDeleteWallet .copyWith( color: Theme.of(context) .extension()! - .textDark3, + .snackBarTextError, ), ), ), diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index f1b967471..c5d682c60 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -140,17 +140,21 @@ class _DesktopSendState extends ConsumerState { const SizedBox( height: 40, ), - Padding( - padding: const EdgeInsets.only( - right: 32, - ), - child: SecondaryButton( - buttonHeight: ButtonHeight.l, - label: "Ok", - onPressed: () { - Navigator.of(context).pop(); - }, - ), + Row( + children: [ + Expanded( + child: SecondaryButton( + buttonHeight: ButtonHeight.l, + label: "Ok", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + const SizedBox( + width: 32, + ), + ], ), ], ), @@ -319,13 +323,13 @@ class _DesktopSendState extends ConsumerState { } if (!wasCancelled && mounted) { + txData["note"] = _note ?? ""; + txData["address"] = _address; // pop building dialog Navigator.of( context, rootNavigator: true, ).pop(); - txData["note"] = _note; - txData["address"] = _address; unawaited( showDialog( @@ -394,22 +398,24 @@ class _DesktopSendState extends ConsumerState { const SizedBox( height: 40, ), - Padding( - padding: const EdgeInsets.only( - right: 32, - ), - child: Expanded( - child: SecondaryButton( - buttonHeight: ButtonHeight.l, - label: "Yes", - onPressed: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(); - }, + Row( + children: [ + Expanded( + child: SecondaryButton( + buttonHeight: ButtonHeight.l, + label: "Ok", + onPressed: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(); + }, + ), ), - ), + const SizedBox( + width: 32, + ), + ], ), ], ), @@ -1002,10 +1008,12 @@ class _DesktopSendState extends ConsumerState { key: const Key("amountInputFieldCryptoTextFieldKey"), controller: cryptoAmountController, focusNode: _cryptoFocus, - keyboardType: const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.right, inputFormatters: [ // regex to validate a crypto amount with 8 decimal places @@ -1056,10 +1064,12 @@ class _DesktopSendState extends ConsumerState { key: const Key("amountInputFieldFiatTextFieldKey"), controller: baseAmountController, focusNode: _baseFocus, - keyboardType: const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.right, inputFormatters: [ // regex to validate a fiat amount with 2 decimal places diff --git a/lib/pages_desktop_specific/settings/settings_menu/nodes_settings.dart b/lib/pages_desktop_specific/settings/settings_menu/nodes_settings.dart index a7e95d33a..8840383d7 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/nodes_settings.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/nodes_settings.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; @@ -46,10 +48,18 @@ class _NodesSettings extends ConsumerState { .toList(); } + final bool isDesktop = Util.isDesktop; + @override void initState() { _coins = _coins.toList(); _coins.remove(Coin.firoTestNet); + if (isDesktop) { + _coins.remove(Coin.wownero); + if (Platform.isWindows) { + _coins.remove(Coin.monero); + } + } searchNodeController = TextEditingController(); searchNodeFocusNode = FocusNode(); @@ -128,8 +138,8 @@ class _NodesSettings extends ConsumerState { Constants.size.circularBorderRadius, ), child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, controller: searchNodeController, focusNode: searchNodeFocusNode, onChanged: (newString) { diff --git a/lib/services/coins/bitcoin/bitcoin_wallet.dart b/lib/services/coins/bitcoin/bitcoin_wallet.dart index 059b73b7d..1ed87681b 100644 --- a/lib/services/coins/bitcoin/bitcoin_wallet.dart +++ b/lib/services/coins/bitcoin/bitcoin_wallet.dart @@ -1297,7 +1297,7 @@ class BitcoinWallet extends CoinServiceAPI { final priceData = await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; - final locale = await Devicelocale.currentLocale; + final locale = Platform.isWindows ? "en_US" : await Devicelocale.currentLocale; final String worthNow = Format.localizedStringAsFixed( value: ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index c5c3f22b1..b18a97186 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -1174,7 +1174,7 @@ class BitcoinCashWallet extends CoinServiceAPI { final priceData = await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; - final locale = await Devicelocale.currentLocale; + final locale = Platform.isWindows ? "en_US" : await Devicelocale.currentLocale; final String worthNow = Format.localizedStringAsFixed( value: ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / @@ -2666,8 +2666,8 @@ class BitcoinCashWallet extends CoinServiceAPI { ], // dust limit is the minimum amount a change output should be ))["vSize"] as int; //todo: check if print needed - debugPrint("vSizeForOneOutput $vSizeForOneOutput"); - debugPrint("vSizeForTwoOutPuts $vSizeForTwoOutPuts"); + // debugPrint("vSizeForOneOutput $vSizeForOneOutput"); + // debugPrint("vSizeForTwoOutPuts $vSizeForTwoOutPuts"); // Assume 1 output, only for recipient and no change var feeForOneOutput = estimateTxFee( diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 41dbed42b..a690157ee 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -178,7 +178,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, - secureStore: secureStorageInterface, + secureStorage: secureStorageInterface, // tracker: tracker, ); @@ -197,7 +197,7 @@ abstract class CoinServiceAPI { walletId: walletId, walletName: walletName, coin: coin, - secureStore: secureStorageInterface, + secureStorage: secureStorageInterface, // tracker: tracker, ); diff --git a/lib/services/coins/dogecoin/dogecoin_wallet.dart b/lib/services/coins/dogecoin/dogecoin_wallet.dart index 696790ad7..349609f18 100644 --- a/lib/services/coins/dogecoin/dogecoin_wallet.dart +++ b/lib/services/coins/dogecoin/dogecoin_wallet.dart @@ -1063,7 +1063,7 @@ class DogecoinWallet extends CoinServiceAPI { final priceData = await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; - final locale = await Devicelocale.currentLocale; + final locale = Platform.isWindows ? "en_US" : await Devicelocale.currentLocale; final String worthNow = Format.localizedStringAsFixed( value: ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index bfddf2560..5d29bd0a9 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -883,7 +883,7 @@ class FiroWallet extends CoinServiceAPI { @override Future updateSentCachedTxData(Map txData) async { final currentPrice = await firoPrice; - final locale = await Devicelocale.currentLocale; + final locale = Platform.isWindows ? "en_US" : await Devicelocale.currentLocale; final String worthNow = Format.localizedStringAsFixed( value: ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / @@ -2756,7 +2756,7 @@ class FiroWallet extends CoinServiceAPI { var price = await firoPrice; var builtHex = txb.build(); // return builtHex; - final locale = await Devicelocale.currentLocale; + final locale =Platform.isWindows ? "en_US" : await Devicelocale.currentLocale; return { "transaction": builtHex, "txid": txId, @@ -2810,7 +2810,7 @@ class FiroWallet extends CoinServiceAPI { final currentPrice = await firoPrice; // Grab the most recent information on all the joinsplits - final locale = await Devicelocale.currentLocale; + final locale = Platform.isWindows ? "en_US" : await Devicelocale.currentLocale; final updatedJSplit = await getJMintTransactions(cachedElectrumXClient, joinsplits, _prefs.currency, coin, currentPrice, locale!); @@ -3249,7 +3249,7 @@ class FiroWallet extends CoinServiceAPI { final currentPrice = await firoPrice; final List> midSortedArray = []; - final locale = await Devicelocale.currentLocale; + final locale = Platform.isWindows ? "en_US" : await Devicelocale.currentLocale; Logging.instance.log("refresh the txs", level: LogLevel.Info); for (final txObject in allTransactions) { @@ -4324,7 +4324,7 @@ class FiroWallet extends CoinServiceAPI { currency, coin, currentPrice, - (await Devicelocale.currentLocale)!); + (Platform.isWindows ? "en_US" : await Devicelocale.currentLocale)!); Logging.instance.log(spendTxs, level: LogLevel.Info); for (var element in spendTxs) { transactionMap[element.txid] = element; @@ -4375,7 +4375,7 @@ class FiroWallet extends CoinServiceAPI { final lelantusEntry = await _getLelantusEntry(); final anonymitySets = await fetchAnonymitySets(); final locktime = await getBlockHead(electrumXClient); - final locale = await Devicelocale.currentLocale; + final locale = Platform.isWindows ? "en_US" : await Devicelocale.currentLocale; ReceivePort receivePort = await getIsolate({ "function": "createJoinSplit", diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index 7f3be9ea1..269f59610 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -1299,7 +1299,7 @@ class LitecoinWallet extends CoinServiceAPI { final priceData = await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; - final locale = await Devicelocale.currentLocale; + final locale = Platform.isWindows ? "en_US" : await Devicelocale.currentLocale; final String worthNow = Format.localizedStringAsFixed( value: ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index 91a6c5ad3..f0869259b 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; -import 'package:cw_core/monero_amount_format.dart'; import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -13,12 +13,10 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart'; -import 'package:cw_monero/api/wallet.dart'; import 'package:cw_monero/monero_wallet.dart'; import 'package:cw_monero/pending_monero_transaction.dart'; -import 'package:dart_numerics/dart_numerics.dart'; import 'package:decimal/decimal.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter_libmonero/core/key_service.dart'; import 'package:flutter_libmonero/core/wallet_creation_service.dart'; import 'package:flutter_libmonero/monero/monero.dart'; @@ -32,7 +30,6 @@ import 'package:stackwallet/models/paymint/transactions_model.dart'; import 'package:stackwallet/models/paymint/utxo_model.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/event_bus/events/global/blocks_remaining_event.dart'; -import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; @@ -51,46 +48,65 @@ import 'package:stackwallet/utilities/stack_file_system.dart'; const int MINIMUM_CONFIRMATIONS = 10; -//https://github.com/monero-project/monero/blob/8361d60aef6e17908658128284899e3a11d808d4/src/cryptonote_config.h#L162 -const String GENESIS_HASH_MAINNET = - "013c01ff0001ffffffffffff03029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1"; -const String GENESIS_HASH_TESTNET = - "013c01ff0001ffffffffffff03029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1"; - class MoneroWallet extends CoinServiceAPI { - static const integrationTestFlag = - bool.fromEnvironment("IS_INTEGRATION_TEST"); - final _prefs = Prefs.instance; - - Timer? timer; - Timer? moneroAutosaveTimer; - late Coin _coin; - - late SecureStorageInterface _secureStore; - - late PriceAPI _priceAPI; - - Future getCurrentNode() async { - return NodeService(secureStorageInterface: _secureStore) - .getPrimaryNodeFor(coin: coin) ?? - DefaultNodes.getNodeFor(coin); - } - - MoneroWallet( - {required String walletId, - required String walletName, - required Coin coin, - PriceAPI? priceAPI, - required SecureStorageInterface secureStore}) { - _walletId = walletId; - _walletName = walletName; - _coin = coin; - - _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = secureStore; - } + final String _walletId; + final Coin _coin; + final PriceAPI _priceAPI; + final SecureStorageInterface _secureStorage; + final Prefs _prefs; + String _walletName; bool _shouldAutoSync = false; + bool _isConnected = false; + bool _hasCalledExit = false; + bool refreshMutex = false; + bool longMutex = false; + + WalletService? walletService; + KeyService? keysStorage; + MoneroWalletBase? walletBase; + WalletCreationService? _walletCreationService; + Timer? _autoSaveTimer; + + Future? _currentReceivingAddress; + Future? _feeObject; + Future? _transactionData; + + Mutex prepareSendMutex = Mutex(); + Mutex estimateFeeMutex = Mutex(); + + MoneroWallet({ + required String walletId, + required String walletName, + required Coin coin, + required SecureStorageInterface secureStorage, + PriceAPI? priceAPI, + Prefs? prefs, + }) : _walletId = walletId, + _walletName = walletName, + _coin = coin, + _priceAPI = priceAPI ?? PriceAPI(Client()), + _secureStorage = secureStorage, + _prefs = prefs ?? Prefs.instance; + + @override + bool get isFavorite { + try { + return DB.instance.get(boxName: walletId, key: "isFavorite") + as bool; + } catch (e, s) { + Logging.instance.log( + "isFavorite fetch failed (returning false by default): $e\n$s", + level: LogLevel.Error); + return false; + } + } + + @override + set isFavorite(bool markFavorite) { + DB.instance.put( + boxName: walletId, key: "isFavorite", value: markFavorite); + } @override bool get shouldAutoSync => _shouldAutoSync; @@ -99,591 +115,235 @@ class MoneroWallet extends CoinServiceAPI { set shouldAutoSync(bool shouldAutoSync) { if (_shouldAutoSync != shouldAutoSync) { _shouldAutoSync = shouldAutoSync; - if (!shouldAutoSync) { - timer?.cancel(); - moneroAutosaveTimer?.cancel(); - timer = null; - moneroAutosaveTimer = null; - stopNetworkAlivePinging(); - } else { - startNetworkAlivePinging(); - // Walletbase needs to be open for this to work - refresh(); - } + // xmr wallets cannot be open at the same time + // leave following commented out for now + + // if (!shouldAutoSync) { + // timer?.cancel(); + // moneroAutosaveTimer?.cancel(); + // timer = null; + // moneroAutosaveTimer = null; + // stopNetworkAlivePinging(); + // } else { + // startNetworkAlivePinging(); + // // Walletbase needs to be open for this to work + // refresh(); + // } } } @override - Future updateNode(bool shouldRefresh) async { - final node = await getCurrentNode(); + String get walletName => _walletName; - final host = Uri.parse(node.host).host; - await walletBase?.connectToNode( - node: Node(uri: "$host:${node.port}", type: WalletType.monero)); + // setter for updating on rename + @override + set walletName(String newName) => _walletName = newName; - // TODO: is this sync call needed? Do we need to notify ui here? - await walletBase?.startSync(); + @override + // not used for monero + Future> get allOwnAddresses async => []; - if (shouldRefresh) { - await refresh(); + @override + Future get availableBalance async { + int runningBalance = 0; + for (final entry in walletBase!.balance!.entries) { + runningBalance += entry.value.unlockedBalance; } - } - - Future> _getMnemonicList() async { - final mnemonicString = - await _secureStore.read(key: '${_walletId}_mnemonic'); - if (mnemonicString == null) { - return []; - } - final List data = mnemonicString.split(' '); - return data; + return Format.satoshisToAmount(runningBalance, coin: coin); } @override - Future> get mnemonic => _getMnemonicList(); - - Future get currentNodeHeight async { - try { - if (walletBase!.syncStatus! is SyncedSyncStatus && - walletBase!.syncStatus!.progress() == 1.0) { - return await walletBase!.getNodeHeight(); - } - } catch (e, s) {} - int _height = -1; - try { - _height = (walletBase!.syncStatus as SyncingSyncStatus).height; - } catch (e, s) { - // Logging.instance.log("$e $s", level: LogLevel.Warning); - } - - int blocksRemaining = -1; - - try { - blocksRemaining = - (walletBase!.syncStatus as SyncingSyncStatus).blocksLeft; - } catch (e, s) { - // Logging.instance.log("$e $s", level: LogLevel.Warning); - } - int currentHeight = _height + blocksRemaining; - if (_height == -1 || blocksRemaining == -1) { - currentHeight = int64MaxValue; - } - final cachedHeight = DB.instance - .get(boxName: walletId, key: "storedNodeHeight") as int? ?? - 0; - - if (currentHeight > cachedHeight && currentHeight != int64MaxValue) { - await DB.instance.put( - boxName: walletId, key: "storedNodeHeight", value: currentHeight); - return currentHeight; - } else { - return cachedHeight; - } - } - - Future get currentSyncingHeight async { - //TODO return the tip of the monero blockchain - try { - if (walletBase!.syncStatus! is SyncedSyncStatus && - walletBase!.syncStatus!.progress() == 1.0) { - // Logging.instance - // .log("currentSyncingHeight lol", level: LogLevel.Warning); - return getSyncingHeight(); - } - } catch (e, s) {} - int syncingHeight = -1; - try { - syncingHeight = (walletBase!.syncStatus as SyncingSyncStatus).height; - } catch (e, s) { - // Logging.instance.log("$e $s", level: LogLevel.Warning); - } - final cachedHeight = - DB.instance.get(boxName: walletId, key: "storedSyncingHeight") - as int? ?? - 0; - - if (syncingHeight > cachedHeight) { - await DB.instance.put( - boxName: walletId, key: "storedSyncingHeight", value: syncingHeight); - return syncingHeight; - } else { - return cachedHeight; - } - } - - Future updateStoredChainHeight({required int newHeight}) async { - await DB.instance.put( - boxName: walletId, key: "storedChainHeight", value: newHeight); - } - - int get storedChainHeight { - return DB.instance.get(boxName: walletId, key: "storedChainHeight") - as int? ?? - 0; - } - - /// Increases the index for either the internal or external chain, depending on [chain]. - /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! - Future _incrementAddressIndexForChain(int chain) async { - // Here we assume chain == 1 if it isn't 0 - String indexKey = chain == 0 ? "receivingIndex" : "changeIndex"; - - final newIndex = - (DB.instance.get(boxName: walletId, key: indexKey)) + 1; - await DB.instance - .put(boxName: walletId, key: indexKey, value: newIndex); - } - - Future _checkCurrentReceivingAddressesForTransactions() async { - try { - await _checkReceivingAddressForTransactions(); - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s", - level: LogLevel.Error); - rethrow; - } - } - - Future _checkReceivingAddressForTransactions() async { - try { - int highestIndex = -1; - for (var element - in walletBase!.transactionHistory!.transactions!.entries) { - if (element.value.direction == TransactionDirection.incoming) { - int curAddressIndex = - element.value.additionalInfo!['addressIndex'] as int; - if (curAddressIndex > highestIndex) { - highestIndex = curAddressIndex; - } - } - } - - // Check the new receiving index - String indexKey = "receivingIndex"; - final curIndex = - DB.instance.get(boxName: walletId, key: indexKey) as int; - if (highestIndex >= curIndex) { - // First increment the receiving index - await _incrementAddressIndexForChain(0); - final newReceivingIndex = - DB.instance.get(boxName: walletId, key: indexKey) as int; - - // Use new index to derive a new receiving address - final newReceivingAddress = - await _generateAddressForChain(0, newReceivingIndex); - - // Add that new receiving address to the array of receiving addresses - await _addToAddressesArrayForChain(newReceivingAddress, 0); - - // Set the new receiving address that the service - - _currentReceivingAddress = Future(() => newReceivingAddress); - } - } on SocketException catch (se, s) { - Logging.instance.log( - "SocketException caught in _checkReceivingAddressForTransactions(): $se\n$s", - level: LogLevel.Error); - return; - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from _checkReceivingAddressForTransactions(): $e\n$s", - level: LogLevel.Error); - rethrow; - } - } + // not used + Future get balanceMinusMaxFee => throw UnimplementedError(); @override - bool get isRefreshing => refreshMutex; - - bool refreshMutex = false; - - Timer? syncPercentTimer; - - Mutex syncHeightMutex = Mutex(); - Future stopSyncPercentTimer() async { - syncPercentTimer?.cancel(); - syncPercentTimer = null; - } - - Future startSyncPercentTimer() async { - if (syncPercentTimer != null) { - return; - } - syncPercentTimer?.cancel(); - GlobalEventBus.instance - .fire(RefreshPercentChangedEvent(highestPercentCached, walletId)); - syncPercentTimer = Timer.periodic(const Duration(seconds: 30), (_) async { - if (syncHeightMutex.isLocked) { - return; - } - await syncHeightMutex.protect(() async { - // int restoreheight = walletBase!.walletInfo.restoreHeight ?? 0; - int _height = await currentSyncingHeight; - int _currentHeight = await currentNodeHeight; - double progress = 0; - try { - progress = walletBase!.syncStatus!.progress(); - } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); - } - - final int blocksRemaining = _currentHeight - _height; - - GlobalEventBus.instance - .fire(BlocksRemainingEvent(blocksRemaining, walletId)); - - if (progress == 1 && _currentHeight > 0 && _height > 0) { - await stopSyncPercentTimer(); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - coin, - ), - ); - return; - } - - // for some reason this can be 0 which screws up the percent calculation - // int64MaxValue is NOT the best value to use here - if (_currentHeight < 1) { - _currentHeight = int64MaxValue; - } - - if (_height < 1) { - _height = 1; - } - - double restorePercent = progress; - double highestPercent = highestPercentCached; + Coin get coin => _coin; + @override + Future confirmSend({required Map txData}) async { + try { + Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info); + final pendingMoneroTransaction = + txData['pendingMoneroTransaction'] as PendingMoneroTransaction; + try { + await pendingMoneroTransaction.commit(); Logging.instance.log( - "currentSyncingHeight: $_height, nodeHeight: $_currentHeight, restorePercent: $restorePercent, highestPercentCached: $highestPercentCached", + "transaction ${pendingMoneroTransaction.id} has been sent", level: LogLevel.Info); - - if (restorePercent > 0 && restorePercent <= 1) { - // if (restorePercent > highestPercent) { - highestPercent = restorePercent; - highestPercentCached = restorePercent; - // } - } - - GlobalEventBus.instance - .fire(RefreshPercentChangedEvent(highestPercent, walletId)); - }); - }); - } - - double get highestPercentCached => - DB.instance.get(boxName: walletId, key: "highestPercentCached") - as double? ?? - 0; - set highestPercentCached(double value) => DB.instance.put( - boxName: walletId, - key: "highestPercentCached", - value: value, - ); - - /// Refreshes display data for the wallet - @override - Future refresh() async { - if (refreshMutex) { - Logging.instance.log("$walletId $walletName refreshMutex denied", - level: LogLevel.Info); - return; - } else { - refreshMutex = true; - } - - if (walletBase == null) { - throw Exception("Tried to call refresh() in monero without walletBase!"); - } - - try { - await startSyncPercentTimer(); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - - final int _currentSyncingHeight = await currentSyncingHeight; - final int storedHeight = storedChainHeight; - int _currentNodeHeight = await currentNodeHeight; - - double progress = 0; - try { - progress = (walletBase!.syncStatus!).progress(); + return pendingMoneroTransaction.id; } catch (e, s) { - // Logging.instance.log("$e $s", level: LogLevel.Warning); - } - await _fetchTransactionData(); - - bool stillSyncing = false; - Logging.instance.log( - "storedHeight: $storedHeight, _currentSyncingHeight: $_currentSyncingHeight, _currentNodeHeight: $_currentNodeHeight, progress: $progress, issynced: ${await walletBase!.isConnected()}", - level: LogLevel.Info); - - if (progress < 1.0) { - stillSyncing = true; - } - - if (_currentSyncingHeight > storedHeight) { - // 0 is returned from monero as I assume an error????? - if (_currentSyncingHeight > 0) { - // 0 failed to fetch current height??? - await updateStoredChainHeight(newHeight: _currentSyncingHeight); - } - } - - await _checkCurrentReceivingAddressesForTransactions(); - String indexKey = "receivingIndex"; - final curIndex = - DB.instance.get(boxName: walletId, key: indexKey) as int; - // Use new index to derive a new receiving address - try { - final newReceivingAddress = await _generateAddressForChain(0, curIndex); - _currentReceivingAddress = Future(() => newReceivingAddress); - } catch (e, s) { - Logging.instance.log( - "Failed to call _generateAddressForChain(0, $curIndex): $e\n$s", + Logging.instance.log("$walletName monero confirmSend: $e\n$s", level: LogLevel.Error); + rethrow; } - final newTxData = await _fetchTransactionData(); - _transactionData = Future(() => newTxData); - - if (isActive || shouldAutoSync) { - timer ??= Timer.periodic(const Duration(seconds: 60), (timer) async { - //todo: check if print needed - // debugPrint("run timer"); - //TODO: check for new data and refresh if needed. if monero even needs this - // chain height check currently broken - // if ((await chainHeight) != (await storedChainHeight)) { - // if (await refreshIfThereIsNewData()) { - await refresh(); - GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( - "New data found in $walletId $walletName in background!", - walletId)); - // } - // } - }); - moneroAutosaveTimer ??= - Timer.periodic(const Duration(seconds: 93), (timer) async { - //todo: check if print needed - // debugPrint("run monero timer"); - if (isActive) { - await walletBase?.save(); - GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( - "New data found in $walletId $walletName in background!", - walletId)); - } - }); - } - - if (stillSyncing) { - debugPrint("still syncing"); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - refreshMutex = false; - return; - } - await stopSyncPercentTimer(); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - coin, - ), - ); - refreshMutex = false; - } catch (error, strace) { - refreshMutex = false; - await stopSyncPercentTimer(); - GlobalEventBus.instance.fire( - NodeConnectionStatusChangedEvent( - NodeConnectionStatus.disconnected, - walletId, - coin, - ), - ); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.unableToSync, - walletId, - coin, - ), - ); - Logging.instance.log( - "Caught exception in refreshWalletData(): $error\n$strace", - level: LogLevel.Error); + } catch (e, s) { + Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", + level: LogLevel.Info); + rethrow; } } - @override - // TODO: implement allOwnAddresses - Future> get allOwnAddresses { - return Future(() => []); - } - - @override - Future get balanceMinusMaxFee async => - (await availableBalance) - - (Decimal.fromInt((await maxFee)) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal(); - @override Future get currentReceivingAddress => _currentReceivingAddress ??= _getCurrentAddressForChain(0); @override - Future exit() async { - _hasCalledExit = true; - stopNetworkAlivePinging(); - moneroAutosaveTimer?.cancel(); - moneroAutosaveTimer = null; - timer?.cancel(); - timer = null; - await stopSyncPercentTimer(); - await walletBase?.save(prioritySave: true); - walletBase?.close(); - isActive = false; + Future estimateFeeFor(int satoshiAmount, int feeRate) async { + MoneroTransactionPriority priority; + + switch (feeRate) { + case 1: + priority = MoneroTransactionPriority.regular; + break; + case 2: + priority = MoneroTransactionPriority.medium; + break; + case 3: + priority = MoneroTransactionPriority.fast; + break; + case 4: + priority = MoneroTransactionPriority.fastest; + break; + case 0: + default: + priority = MoneroTransactionPriority.slow; + break; + } + + final fee = walletBase!.calculateEstimatedFee(priority, satoshiAmount); + + return fee; } - bool _hasCalledExit = false; - @override - bool get hasCalledExit => _hasCalledExit; - - Future? _currentReceivingAddress; - - Future _getFees() async { - // TODO: not use random hard coded values here - return FeeObject( - numberOfBlocksFast: 10, - numberOfBlocksAverage: 15, - numberOfBlocksSlow: 20, - fast: MoneroTransactionPriority.fast.raw!, - medium: MoneroTransactionPriority.regular.raw!, - slow: MoneroTransactionPriority.slow.raw!, - ); + Future exit() async { + if (!_hasCalledExit) { + walletBase?.onNewBlock = null; + walletBase?.onNewTransaction = null; + walletBase?.syncStatusChanged = null; + _hasCalledExit = true; + _autoSaveTimer?.cancel(); + await walletBase?.save(prioritySave: true); + walletBase?.close(); + } } @override Future get fees => _feeObject ??= _getFees(); - Future? _feeObject; @override - // TODO: implement fullRescan Future fullRescan( int maxUnusedAddressGap, int maxNumberOfIndexesToCheck, ) async { var restoreHeight = walletBase?.walletInfo.restoreHeight; + highestPercentCached = 0; await walletBase?.rescan(height: restoreHeight); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - return; } - Future _generateAddressForChain(int chain, int index) async { - // - String address = walletBase!.getTransactionAddress(chain, index); + @override + Future generateNewAddress() async { + try { + const String indexKey = "receivingIndex"; + // First increment the receiving index + await _incrementAddressIndexForChain(0); + final newReceivingIndex = + DB.instance.get(boxName: walletId, key: indexKey) as int; - return address; - } + // Use new index to derive a new receiving address + final newReceivingAddress = + await _generateAddressForChain(0, newReceivingIndex); - /// Adds [address] to the relevant chain's address array, which is determined by [chain]. - /// [address] - Expects a standard native segwit address - /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! - Future _addToAddressesArrayForChain(String address, int chain) async { - String chainArray = ''; - if (chain == 0) { - chainArray = 'receivingAddresses'; - } else { - chainArray = 'changeAddresses'; - } + // Add that new receiving address to the array of receiving addresses + await _addToAddressesArrayForChain(newReceivingAddress, 0); - final addressArray = - DB.instance.get(boxName: walletId, key: chainArray); - if (addressArray == null) { + // Set the new receiving address that the service + + _currentReceivingAddress = Future(() => newReceivingAddress); + + return true; + } catch (e, s) { Logging.instance.log( - 'Attempting to add the following to $chainArray array for chain $chain:${[ - address - ]}', - level: LogLevel.Info); - await DB.instance - .put(boxName: walletId, key: chainArray, value: [address]); - } else { - // Make a deep copy of the existing list - final List newArray = []; - addressArray - .forEach((dynamic _address) => newArray.add(_address as String)); - newArray.add(address); // Add the address passed into the method - await DB.instance - .put(boxName: walletId, key: chainArray, value: newArray); + "Exception rethrown from generateNewAddress(): $e\n$s", + level: LogLevel.Error); + return false; } } - /// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain] - /// and - /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! - Future _getCurrentAddressForChain(int chain) async { - // Here, we assume that chain == 1 if it isn't 0 - String arrayKey = chain == 0 ? "receivingAddresses" : "changeAddresses"; - final internalChainArray = (DB.instance - .get(boxName: walletId, key: arrayKey)) as List; - return internalChainArray.last as String; - } + @override + bool get hasCalledExit => _hasCalledExit; - //TODO: take in the default language when creating wallet. - Future _generateNewWallet() async { - Logging.instance - .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); - // TODO: ping monero server and make sure the genesis hash matches - // if (!integrationTestFlag) { - // final features = await electrumXClient.getServerFeatures(); - // Logging.instance.log("features: $features"); - // if (_networkType == BasicNetworkType.main) { - // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { - // throw Exception("genesis hash does not match main net!"); - // } - // } else if (_networkType == BasicNetworkType.test) { - // if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { - // throw Exception("genesis hash does not match test net!"); - // } - // } + @override + Future initializeExisting() async { + Logging.instance.log( + "Opening existing ${coin.prettyName} wallet $walletName...", + level: LogLevel.Info, + ); + + if ((DB.instance.get(boxName: walletId, key: "id")) == null) { + throw Exception( + "Attempted to initialize an existing wallet using an unknown wallet ID!"); + } + + walletService = + monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); + keysStorage = KeyService(_secureStorage); + + await _prefs.init(); + // final data = + // DB.instance.get(boxName: walletId, key: "latest_tx_model") + // as TransactionData?; + // if (data != null) { + // _transactionData = Future(() => data); // } + String password; + try { + password = await keysStorage!.getWalletPassword(walletName: _walletId); + } catch (_) { + throw Exception("Monero password not found for $walletName"); + } + walletBase = (await walletService!.openWallet(_walletId, password)) + as MoneroWalletBase; + // walletBase!.onNewBlock = onNewBlock; + // walletBase!.onNewTransaction = onNewTransaction; + // walletBase!.syncStatusChanged = syncStatusChanged; + Logging.instance.log( + "Opened existing ${coin.prettyName} wallet $walletName", + level: LogLevel.Info, + ); + // Wallet already exists, triggers for a returning user + + String indexKey = "receivingIndex"; + final curIndex = + await DB.instance.get(boxName: walletId, key: indexKey) as int; + // Use new index to derive a new receiving address + final newReceivingAddress = await _generateAddressForChain(0, curIndex); + Logging.instance.log("xmr address in init existing: $newReceivingAddress", + level: LogLevel.Info); + _currentReceivingAddress = Future(() => newReceivingAddress); + } + + @override + Future initializeNew() async { + await _prefs.init(); + // this should never fail - if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) { + if ((await _secureStorage.read(key: '${_walletId}_mnemonic')) != null) { throw Exception( "Attempted to overwrite mnemonic on generate new wallet!"); } walletService = monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); - keysStorage = KeyService(_secureStore); + keysStorage = KeyService(_secureStorage); WalletInfo walletInfo; WalletCredentials credentials; try { String name = _walletId; final dirPath = - await pathForWalletDir(name: name, type: WalletType.monero); - final path = await pathForWallet(name: name, type: WalletType.monero); + await _pathForWalletDir(name: name, type: WalletType.monero); + final path = await _pathForWallet(name: name, type: WalletType.monero); credentials = monero.createMoneroNewWalletCredentials( name: name, language: "English", @@ -710,7 +370,7 @@ class MoneroWallet extends CoinServiceAPI { credentials.walletInfo = walletInfo; _walletCreationService = WalletCreationService( - secureStorage: _secureStore, + secureStorage: _secureStorage, walletService: walletService, keyService: keysStorage, ); @@ -718,23 +378,28 @@ class MoneroWallet extends CoinServiceAPI { // To restore from a seed final wallet = await _walletCreationService?.create(credentials); - await _secureStore.write( + await _secureStorage.write( key: '${_walletId}_mnemonic', value: wallet?.seed.trim()); walletInfo.address = wallet?.walletAddresses.address; await DB.instance .add(boxName: WalletInfo.boxName, value: walletInfo); walletBase?.close(); walletBase = wallet as MoneroWalletBase; + // walletBase!.onNewBlock = onNewBlock; + // walletBase!.onNewTransaction = onNewTransaction; + // walletBase!.syncStatusChanged = syncStatusChanged; } catch (e, s) { //todo: come back to this + debugPrint("some nice searchable string thing"); debugPrint(e.toString()); debugPrint(s.toString()); + walletBase?.close(); } - final node = await getCurrentNode(); + final node = await _getCurrentNode(); final host = Uri.parse(node.host).host; - await walletBase?.connectToNode( + await walletBase!.connectToNode( node: Node(uri: "$host:${node.port}", type: WalletType.monero)); - await walletBase?.startSync(); + await walletBase!.startSync(); await DB.instance .put(boxName: walletId, key: "id", value: _walletId); @@ -771,663 +436,42 @@ class MoneroWallet extends CoinServiceAPI { .put(boxName: walletId, key: "receivingIndex", value: 0); _currentReceivingAddress = Future(() => initialReceivingAddress); - - Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info); + walletBase?.close(); + Logging.instance + .log("initializeNew for $walletName $walletId", level: LogLevel.Info); } - @override - // TODO: implement initializeWallet - Future initializeNew() async { - await _prefs.init(); - // TODO: ping actual monero network - // try { - // final hasNetwork = await _electrumXClient.ping(); - // if (!hasNetwork) { - // return false; - // } - // } catch (e, s) { - // Logging.instance.log("Caught in initializeWallet(): $e\n$s"); - // return false; - // } - - walletService = - monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); - keysStorage = KeyService(_secureStore); - - await _generateNewWallet(); - // var password; - // try { - // password = - // await keysStorage?.getWalletPassword(walletName: this._walletId); - // } catch (e, s) { - // Logging.instance.log("$e $s"); - // Logging.instance.log("Generating new ${coin.ticker} wallet."); - // // Triggers for new users automatically. Generates new wallet - // await _generateNewWallet(wallet); - // await wallet.put("id", this._walletId); - // return true; - // } - // walletBase = (await walletService?.openWallet(this._walletId, password)) - // as MoneroWalletBase; - // Logging.instance.log("Opening existing ${coin.ticker} wallet."); - // // Wallet already exists, triggers for a returning user - // final currentAddress = awaicurrentHeightt _getCurrentAddressForChain(0); - // this._currentReceivingAddress = Future(() => currentAddress); - // - // await walletBase?.connectToNode( - // node: Node( - // uri: "xmr-node.cakewallet.com:18081", type: WalletType.monero)); - // walletBase?.startSync(); - - return true; - } - - @override - Future initializeExisting() async { - Logging.instance.log( - "Opening existing ${coin.prettyName} wallet $walletName...", - level: LogLevel.Info); - - if ((DB.instance.get(boxName: walletId, key: "id")) == null) { - //todo: check if print is needed - // debugPrint("Exception was thrown"); - throw Exception( - "Attempted to initialize an existing wallet using an unknown wallet ID!"); - } - - walletService = - monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); - keysStorage = KeyService(_secureStore); - - await _prefs.init(); - final data = - DB.instance.get(boxName: walletId, key: "latest_tx_model") - as TransactionData?; - if (data != null) { - _transactionData = Future(() => data); - } - - String? password; - try { - password = await keysStorage?.getWalletPassword(walletName: _walletId); - } catch (e, s) { - //todo: check if print needed - // debugPrint("Exception was thrown $e $s"); - throw Exception("Password not found $e, $s"); - } - walletBase = (await walletService?.openWallet(_walletId, password!)) - as MoneroWalletBase; - debugPrint("walletBase $walletBase"); - Logging.instance.log( - "Opened existing ${coin.prettyName} wallet $walletName", - level: LogLevel.Info); - // Wallet already exists, triggers for a returning user - - String indexKey = "receivingIndex"; - final curIndex = - await DB.instance.get(boxName: walletId, key: indexKey) as int; - // Use new index to derive a new receiving address - final newReceivingAddress = await _generateAddressForChain(0, curIndex); - Logging.instance.log("xmr address in init existing: $newReceivingAddress", - level: LogLevel.Info); - _currentReceivingAddress = Future(() => newReceivingAddress); - } - - @override - Future get maxFee async { - var bal = await availableBalance; - var fee = walletBase!.calculateEstimatedFee( - monero.getDefaultTransactionPriority(), - Format.decimalAmountToSatoshis(bal, coin), - ); - - return fee; - } - - @override - // TODO: implement pendingBalance - Future get pendingBalance => throw UnimplementedError(); - - bool longMutex = false; - - // TODO: are these needed? - - WalletService? walletService; - KeyService? keysStorage; - MoneroWalletBase? walletBase; - WalletCreationService? _walletCreationService; - - String toStringForinfo(WalletInfo info) { - return "id: ${info.id} name: ${info.name} type: ${info.type} recovery: ${info.isRecovery}" - " restoreheight: ${info.restoreHeight} timestamp: ${info.timestamp} dirPath: ${info.dirPath} " - "path: ${info.path} address: ${info.address} addresses: ${info.addresses}"; - } - - Future pathForWalletDir({ - required String name, - required WalletType type, - }) async { - Directory root = await StackFileSystem.applicationRootDirectory(); - - final prefix = walletTypeToString(type).toLowerCase(); - final walletsDir = Directory('${root.path}/wallets'); - final walletDire = Directory('${walletsDir.path}/$prefix/$name'); - - if (!walletDire.existsSync()) { - walletDire.createSync(recursive: true); - } - - return walletDire.path; - } - - Future pathForWallet({ - required String name, - required WalletType type, - }) async => - await pathForWalletDir(name: name, type: type) - .then((path) => '$path/$name'); - - // TODO: take in a dynamic height - @override - Future recoverFromMnemonic({ - required String mnemonic, - required int maxUnusedAddressGap, - required int maxNumberOfIndexesToCheck, - required int height, - }) async { - await _prefs.init(); - longMutex = true; - final start = DateTime.now(); - try { - // Logging.instance.log("IS_INTEGRATION_TEST: $integrationTestFlag"); - // if (!integrationTestFlag) { - // final features = await electrumXClient.getServerFeatures(); - // Logging.instance.log("features: $features"); - // if (_networkType == BasicNetworkType.main) { - // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { - // throw Exception("genesis hash does not match main net!"); - // } - // } else if (_networkType == BasicNetworkType.test) { - // if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { - // throw Exception("genesis hash does not match test net!"); - // } - // } - // } - // check to make sure we aren't overwriting a mnemonic - // this should never fail - if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) { - longMutex = false; - throw Exception("Attempted to overwrite mnemonic on restore!"); - } - await _secureStore.write( - key: '${_walletId}_mnemonic', value: mnemonic.trim()); - - await DB.instance - .put(boxName: walletId, key: "restoreHeight", value: height); - - walletService = - monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); - keysStorage = KeyService(_secureStore); - WalletInfo walletInfo; - WalletCredentials credentials; - String name = _walletId; - final dirPath = - await pathForWalletDir(name: name, type: WalletType.monero); - final path = await pathForWallet(name: name, type: WalletType.monero); - credentials = monero.createMoneroRestoreWalletFromSeedCredentials( - name: name, - height: height, - mnemonic: mnemonic.trim(), - ); - try { - walletInfo = WalletInfo.external( - id: WalletBase.idFor(name, WalletType.monero), - name: name, - type: WalletType.monero, - isRecovery: false, - restoreHeight: credentials.height ?? 0, - date: DateTime.now(), - path: path, - dirPath: dirPath, - // TODO: find out what to put for address - address: ''); - credentials.walletInfo = walletInfo; - - _walletCreationService = WalletCreationService( - secureStorage: _secureStore, - walletService: walletService, - keyService: keysStorage, - ); - _walletCreationService!.changeWalletType(); - // To restore from a seed - final wallet = - await _walletCreationService!.restoreFromSeed(credentials); - walletInfo.address = wallet.walletAddresses.address; - await DB.instance - .add(boxName: WalletInfo.boxName, value: walletInfo); - walletBase?.close(); - walletBase = wallet as MoneroWalletBase; - await DB.instance.put( - boxName: walletId, - key: 'receivingAddresses', - value: [walletInfo.address!]); - await DB.instance - .put(boxName: walletId, key: "receivingIndex", value: 0); - await DB.instance - .put(boxName: walletId, key: "id", value: _walletId); - await DB.instance - .put(boxName: walletId, key: "changeIndex", value: 0); - await DB.instance.put( - boxName: walletId, - key: 'blocked_tx_hashes', - value: ["0xdefault"], - ); // A list of transaction hashes to represent frozen utxos in wallet - // initialize address book entries - await DB.instance.put( - boxName: walletId, - key: 'addressBookEntries', - value: {}); - await DB.instance - .put(boxName: walletId, key: "isFavorite", value: false); - } catch (e, s) { - debugPrint(e.toString()); - debugPrint(s.toString()); - } - final node = await getCurrentNode(); - final host = Uri.parse(node.host).host; - await walletBase?.connectToNode( - node: Node(uri: "$host:${node.port}", type: WalletType.monero)); - await walletBase?.rescan(height: credentials.height); - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from recoverFromMnemonic(): $e\n$s", - level: LogLevel.Error); - longMutex = false; - rethrow; - } - longMutex = false; - - final end = DateTime.now(); - Logging.instance.log( - "$walletName Recovery time: ${end.difference(start).inMilliseconds} millis", - level: LogLevel.Info); - } - - @override - Future send({ - required String toAddress, - required int amount, - Map args = const {}, - }) async { - try { - final txData = await prepareSend( - address: toAddress, satoshiAmount: amount, args: args); - final txHash = await confirmSend(txData: txData); - return txHash; - } catch (e, s) { - Logging.instance - .log("Exception rethrown from send(): $e\n$s", level: LogLevel.Error); - rethrow; - } - } - - @override - Future testNetworkConnection() async { - return await walletBase?.isConnected() ?? false; - } - - Timer? _networkAliveTimer; - - void startNetworkAlivePinging() { - // call once on start right away - _periodicPingCheck(); - - // then periodically check - _networkAliveTimer = Timer.periodic( - Constants.networkAliveTimerDuration, - (_) async { - _periodicPingCheck(); - }, - ); - } - - void _periodicPingCheck() async { - bool hasNetwork = await testNetworkConnection(); - _isConnected = hasNetwork; - if (_isConnected != hasNetwork) { - NodeConnectionStatus status = hasNetwork - ? NodeConnectionStatus.connected - : NodeConnectionStatus.disconnected; - GlobalEventBus.instance - .fire(NodeConnectionStatusChangedEvent(status, walletId, coin)); - } - } - - void stopNetworkAlivePinging() { - _networkAliveTimer?.cancel(); - _networkAliveTimer = null; - } - - bool _isConnected = false; - @override bool get isConnected => _isConnected; @override - Future get totalBalance async { - var transactions = walletBase?.transactionHistory!.transactions; - int transactionBalance = 0; - for (var tx in transactions!.entries) { - if (tx.value.direction == TransactionDirection.incoming) { - transactionBalance += tx.value.amount!; - } else { - transactionBalance += -tx.value.amount! - tx.value.fee!; - } - } + bool get isRefreshing => refreshMutex; - // TODO: grab total balance - var bal = 0; - for (var element in walletBase!.balance!.entries) { - bal = bal + element.value.fullBalance; - } - //todo: check if print needed - // debugPrint("balances: $transactionBalance $bal"); - if (isActive) { - String am = moneroAmountToString(amount: bal); + @override + // not used in xmr + Future get maxFee => throw UnimplementedError(); - return Decimal.parse(am); - } else { - String am = moneroAmountToString(amount: transactionBalance); - - return Decimal.parse(am); + @override + Future> get mnemonic async { + final mnemonicString = + await _secureStorage.read(key: '${_walletId}_mnemonic'); + if (mnemonicString == null) { + return []; } + final List data = mnemonicString.split(' '); + return data; } @override - // TODO: implement onIsActiveWalletChanged - void Function(bool)? get onIsActiveWalletChanged => (isActive) async { - await walletBase?.save(); - walletBase?.close(); - moneroAutosaveTimer?.cancel(); - moneroAutosaveTimer = null; - timer?.cancel(); - timer = null; - await stopSyncPercentTimer(); - if (isActive) { - String? password; - try { - password = - await keysStorage?.getWalletPassword(walletName: _walletId); - } catch (e, s) { - //todo: check if print needed - // debugPrint("Exception was thrown $e $s"); - throw Exception("Password not found $e, $s"); - } - walletBase = (await walletService?.openWallet(_walletId, password!)) - as MoneroWalletBase?; - if (!(await walletBase!.isConnected())) { - final node = await getCurrentNode(); - final host = Uri.parse(node.host).host; - await walletBase?.connectToNode( - node: Node(uri: "$host:${node.port}", type: WalletType.monero)); - await walletBase?.startSync(); - } - await refresh(); - } - this.isActive = isActive; - }; - - bool isActive = false; + // not used in xmr + Future get pendingBalance => throw UnimplementedError(); @override - Future get transactionData => - _transactionData ??= _fetchTransactionData(); - Future? _transactionData; - - // not used in monero - TransactionData? cachedTxData; - - @override - Future updateSentCachedTxData(Map txData) async { - // not used in monero - } - - Future _fetchTransactionData() async { - final transactions = walletBase?.transactionHistory!.transactions; - - final cachedTransactions = - DB.instance.get(boxName: walletId, key: 'latest_tx_model') - as TransactionData?; - int latestTxnBlockHeight = - DB.instance.get(boxName: walletId, key: "storedTxnDataHeight") - as int? ?? - 0; - - final txidsList = DB.instance - .get(boxName: walletId, key: "cachedTxids") as List? ?? - []; - - final Set cachedTxids = Set.from(txidsList); - - // TODO: filter to skip cached + confirmed txn processing in next step - // final unconfirmedCachedTransactions = - // cachedTransactions?.getAllTransactions() ?? {}; - // unconfirmedCachedTransactions - // .removeWhere((key, value) => value.confirmedStatus); - // - // if (cachedTransactions != null) { - // for (final tx in allTxHashes.toList(growable: false)) { - // final txHeight = tx["height"] as int; - // if (txHeight > 0 && - // txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) { - // if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) { - // allTxHashes.remove(tx); - // } - // } - // } - // } - - // sort thing stuff - // change to get Monero price - final priceData = - await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); - Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; - final List> midSortedArray = []; - - if (transactions != null) { - for (var tx in transactions.entries) { - cachedTxids.add(tx.value.id); - Logging.instance.log( - "${tx.value.accountIndex} ${tx.value.addressIndex} ${tx.value.amount} ${tx.value.date} " - "${tx.value.direction} ${tx.value.fee} ${tx.value.height} ${tx.value.id} ${tx.value.isPending} ${tx.value.key} " - "${tx.value.recipientAddress}, ${tx.value.additionalInfo} con:${tx.value.confirmations}" - " ${tx.value.keyIndex}", - level: LogLevel.Info); - String am = moneroAmountToString(amount: tx.value.amount!); - final worthNow = (currentPrice * Decimal.parse(am)).toStringAsFixed(2); - Map midSortedTx = {}; - // // create final tx map - midSortedTx["txid"] = tx.value.id; - midSortedTx["confirmed_status"] = !tx.value.isPending && - tx.value.confirmations! >= MINIMUM_CONFIRMATIONS; - midSortedTx["confirmations"] = tx.value.confirmations ?? 0; - midSortedTx["timestamp"] = - (tx.value.date.millisecondsSinceEpoch ~/ 1000); - midSortedTx["txType"] = - tx.value.direction == TransactionDirection.incoming - ? "Received" - : "Sent"; - midSortedTx["amount"] = tx.value.amount; - midSortedTx["worthNow"] = worthNow; - midSortedTx["worthAtBlockTimestamp"] = worthNow; - midSortedTx["fees"] = tx.value.fee; - // TODO: shouldn't monero have an address I can grab - if (tx.value.direction == TransactionDirection.incoming) { - final addressInfo = tx.value.additionalInfo; - - midSortedTx["address"] = walletBase?.getTransactionAddress( - addressInfo!['accountIndex'] as int, - addressInfo['addressIndex'] as int, - ); - } else { - midSortedTx["address"] = ""; - } - - final int txHeight = tx.value.height ?? 0; - midSortedTx["height"] = txHeight; - if (txHeight >= latestTxnBlockHeight) { - latestTxnBlockHeight = txHeight; - } - - midSortedTx["aliens"] = []; - midSortedTx["inputSize"] = 0; - midSortedTx["outputSize"] = 0; - midSortedTx["inputs"] = []; - midSortedTx["outputs"] = []; - midSortedArray.add(midSortedTx); - } - } - - // sort by date ---- - midSortedArray - .sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int)); - Logging.instance.log(midSortedArray, level: LogLevel.Info); - - // buildDateTimeChunks - final Map result = {"dateTimeChunks": []}; - final dateArray = []; - - for (int i = 0; i < midSortedArray.length; i++) { - final txObject = midSortedArray[i]; - final date = extractDateFromTimestamp(txObject["timestamp"] as int); - final txTimeArray = [txObject["timestamp"], date]; - - if (dateArray.contains(txTimeArray[1])) { - result["dateTimeChunks"].forEach((dynamic chunk) { - if (extractDateFromTimestamp(chunk["timestamp"] as int) == - txTimeArray[1]) { - if (chunk["transactions"] == null) { - chunk["transactions"] = >[]; - } - chunk["transactions"].add(txObject); - } - }); - } else { - dateArray.add(txTimeArray[1]); - final chunk = { - "timestamp": txTimeArray[0], - "transactions": [txObject], - }; - result["dateTimeChunks"].add(chunk); - } - } - - final transactionsMap = cachedTransactions?.getAllTransactions() ?? {}; - transactionsMap - .addAll(TransactionData.fromJson(result).getAllTransactions()); - - final txModel = TransactionData.fromMap(transactionsMap); - - await DB.instance.put( - boxName: walletId, - key: 'storedTxnDataHeight', - value: latestTxnBlockHeight); - await DB.instance.put( - boxName: walletId, key: 'latest_tx_model', value: txModel); - await DB.instance.put( - boxName: walletId, - key: 'cachedTxids', - value: cachedTxids.toList(growable: false)); - - return txModel; - } - - @override - // TODO: implement unspentOutputs - Future> get unspentOutputs => throw UnimplementedError(); - - @override - bool validateAddress(String address) { - bool valid = walletBase!.validateAddress(address); - return valid; - } - - @override - String get walletId => _walletId; - late String _walletId; - - @override - String get walletName => _walletName; - late String _walletName; - - // setter for updating on rename - @override - set walletName(String newName) => _walletName = newName; - - @override - set isFavorite(bool markFavorite) { - DB.instance.put( - boxName: walletId, key: "isFavorite", value: markFavorite); - } - - @override - bool get isFavorite { - try { - return DB.instance.get(boxName: walletId, key: "isFavorite") - as bool; - } catch (e, s) { - Logging.instance.log( - "isFavorite fetch failed (returning false by default): $e\n$s", - level: LogLevel.Error); - return false; - } - } - - @override - Future get availableBalance async { - var bal = 0; - for (var element in walletBase!.balance!.entries) { - bal = bal + element.value.unlockedBalance; - } - String am = moneroAmountToString(amount: bal); - - return Decimal.parse(am); - } - - @override - Coin get coin => _coin; - - @override - Future confirmSend({required Map txData}) async { - try { - Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info); - final pendingMoneroTransaction = - txData['pendingMoneroTransaction'] as PendingMoneroTransaction; - try { - await pendingMoneroTransaction.commit(); - Logging.instance.log( - "transaction ${pendingMoneroTransaction.id} has been sent", - level: LogLevel.Info); - return pendingMoneroTransaction.id; - } catch (e, s) { - Logging.instance.log("$walletName monero confirmSend: $e\n$s", - level: LogLevel.Error); - rethrow; - } - } catch (e, s) { - Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s", - level: LogLevel.Info); - rethrow; - } - } - - // TODO: fix the double free memory crash error. - @override - Future> prepareSend( - {required String address, - required int satoshiAmount, - Map? args}) async { - int amount = satoshiAmount; + Future> prepareSend({ + required String address, + required int satoshiAmount, + Map? args, + }) async { String toAddress = address; try { final feeRate = args?["feeRate"]; @@ -1450,16 +494,17 @@ class MoneroWallet extends CoinServiceAPI { // check for send all bool isSendAll = false; final balance = await availableBalance; - final satInDecimal = ((Decimal.fromInt(satoshiAmount) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal()); + final satInDecimal = + Format.satoshisToAmount(satoshiAmount, coin: coin); if (satInDecimal == balance) { isSendAll = true; } Logging.instance - .log("$toAddress $amount $args", level: LogLevel.Info); - String amountToSend = moneroAmountToString(amount: amount); - Logging.instance.log("$amount $amountToSend", level: LogLevel.Info); + .log("$toAddress $satoshiAmount $args", level: LogLevel.Info); + String amountToSend = satInDecimal + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + Logging.instance + .log("$satoshiAmount $amountToSend", level: LogLevel.Info); monero_output.Output output = monero_output.Output(walletBase!); output.address = toAddress; @@ -1510,89 +555,740 @@ class MoneroWallet extends CoinServiceAPI { } } - Mutex prepareSendMutex = Mutex(); - Mutex estimateFeeMutex = Mutex(); - @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { - MoneroTransactionPriority priority; - FeeRateType feeRateType; - - switch (feeRate) { - case 1: - priority = MoneroTransactionPriority.regular; - feeRateType = FeeRateType.average; - break; - case 2: - priority = MoneroTransactionPriority.medium; - feeRateType = FeeRateType.average; - break; - case 3: - priority = MoneroTransactionPriority.fast; - feeRateType = FeeRateType.fast; - break; - case 4: - priority = MoneroTransactionPriority.fastest; - feeRateType = FeeRateType.fast; - break; - case 0: - default: - priority = MoneroTransactionPriority.slow; - feeRateType = FeeRateType.slow; - break; - } - // int? aprox; - - // corrupted size vs. prev_size occurs but not sure if related to fees or just generating monero transactions in general - - // await estimateFeeMutex.protect(() async { - // { - // try { - // aprox = (await prepareSend( - // // This address is only used for getting an approximate fee, never for sending - // address: - // "8347huhmj6Ggzr1BpZPJAD5oa96ob5Fe8GtQdGZDYVVYVsCgtUNH3pEEzExDuaAVZdC16D4FkAb24J6wUfsKkcZtC8EPXB7", - // satoshiAmount: satoshiAmount, - // args: {"feeRate": feeRateType}))['fee'] as int?; - // await Future.delayed(const Duration(milliseconds: 1000)); - // } catch (e, s) { - // Logging.instance.log("$feeRateType $e $s", level: LogLevel.Error); - final aprox = walletBase!.calculateEstimatedFee(priority, satoshiAmount); - // } - // } - // }); - - print("this is the aprox fee $aprox for $satoshiAmount"); - final fee = aprox; - return fee; - } - - @override - Future generateNewAddress() async { + Future recoverFromMnemonic({ + required String mnemonic, + required int maxUnusedAddressGap, + required int maxNumberOfIndexesToCheck, + required int height, + }) async { + await _prefs.init(); + longMutex = true; + final start = DateTime.now(); try { - const String indexKey = "receivingIndex"; - // First increment the receiving index - await _incrementAddressIndexForChain(0); - final newReceivingIndex = - DB.instance.get(boxName: walletId, key: indexKey) as int; + // Logging.instance.log("IS_INTEGRATION_TEST: $integrationTestFlag"); + // if (!integrationTestFlag) { + // final features = await electrumXClient.getServerFeatures(); + // Logging.instance.log("features: $features"); + // if (_networkType == BasicNetworkType.main) { + // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + // throw Exception("genesis hash does not match main net!"); + // } + // } else if (_networkType == BasicNetworkType.test) { + // if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { + // throw Exception("genesis hash does not match test net!"); + // } + // } + // } + // check to make sure we aren't overwriting a mnemonic + // this should never fail + if ((await _secureStorage.read(key: '${_walletId}_mnemonic')) != null) { + longMutex = false; + throw Exception("Attempted to overwrite mnemonic on restore!"); + } + await _secureStorage.write( + key: '${_walletId}_mnemonic', value: mnemonic.trim()); - // Use new index to derive a new receiving address - final newReceivingAddress = - await _generateAddressForChain(0, newReceivingIndex); + await DB.instance + .put(boxName: walletId, key: "restoreHeight", value: height); - // Add that new receiving address to the array of receiving addresses - await _addToAddressesArrayForChain(newReceivingAddress, 0); + walletService = + monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); + keysStorage = KeyService(_secureStorage); + WalletInfo walletInfo; + WalletCredentials credentials; + String name = _walletId; + final dirPath = + await _pathForWalletDir(name: name, type: WalletType.monero); + final path = await _pathForWallet(name: name, type: WalletType.monero); + credentials = monero.createMoneroRestoreWalletFromSeedCredentials( + name: name, + height: height, + mnemonic: mnemonic.trim(), + ); + try { + walletInfo = WalletInfo.external( + id: WalletBase.idFor(name, WalletType.monero), + name: name, + type: WalletType.monero, + isRecovery: false, + restoreHeight: credentials.height ?? 0, + date: DateTime.now(), + path: path, + dirPath: dirPath, + // TODO: find out what to put for address + address: ''); + credentials.walletInfo = walletInfo; - // Set the new receiving address that the service - - _currentReceivingAddress = Future(() => newReceivingAddress); - - return true; + _walletCreationService = WalletCreationService( + secureStorage: _secureStorage, + walletService: walletService, + keyService: keysStorage, + ); + _walletCreationService!.changeWalletType(); + // To restore from a seed + final wallet = + await _walletCreationService!.restoreFromSeed(credentials); + walletInfo.address = wallet.walletAddresses.address; + await DB.instance + .add(boxName: WalletInfo.boxName, value: walletInfo); + walletBase?.close(); + walletBase = wallet as MoneroWalletBase; + // walletBase!.onNewBlock = onNewBlock; + // walletBase!.onNewTransaction = onNewTransaction; + // walletBase!.syncStatusChanged = syncStatusChanged; + await DB.instance.put( + boxName: walletId, + key: 'receivingAddresses', + value: [walletInfo.address!]); + await DB.instance + .put(boxName: walletId, key: "receivingIndex", value: 0); + await DB.instance + .put(boxName: walletId, key: "id", value: _walletId); + await DB.instance + .put(boxName: walletId, key: "changeIndex", value: 0); + await DB.instance.put( + boxName: walletId, + key: 'blocked_tx_hashes', + value: ["0xdefault"], + ); // A list of transaction hashes to represent frozen utxos in wallet + // initialize address book entries + await DB.instance.put( + boxName: walletId, + key: 'addressBookEntries', + value: {}); + await DB.instance + .put(boxName: walletId, key: "isFavorite", value: false); + } catch (e, s) { + debugPrint(e.toString()); + debugPrint(s.toString()); + } + final node = await _getCurrentNode(); + final host = Uri.parse(node.host).host; + await walletBase!.connectToNode( + node: Node(uri: "$host:${node.port}", type: WalletType.monero)); + await walletBase!.rescan(height: credentials.height); + walletBase!.close(); } catch (e, s) { Logging.instance.log( - "Exception rethrown from generateNewAddress(): $e\n$s", + "Exception rethrown from recoverFromMnemonic(): $e\n$s", level: LogLevel.Error); - return false; + longMutex = false; + rethrow; + } + longMutex = false; + + final end = DateTime.now(); + Logging.instance.log( + "$walletName Recovery time: ${end.difference(start).inMilliseconds} millis", + level: LogLevel.Info); + } + + @override + Future refresh() async { + if (refreshMutex) { + Logging.instance.log("$walletId $walletName refreshMutex denied", + level: LogLevel.Info); + return; + } else { + refreshMutex = true; + } + + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + + final newTxData = await _fetchTransactionData(); + _transactionData = Future(() => newTxData); + + await _checkCurrentReceivingAddressesForTransactions(); + String indexKey = "receivingIndex"; + final curIndex = + DB.instance.get(boxName: walletId, key: indexKey) as int; + // Use new index to derive a new receiving address + try { + final newReceivingAddress = await _generateAddressForChain(0, curIndex); + _currentReceivingAddress = Future(() => newReceivingAddress); + } catch (e, s) { + Logging.instance.log( + "Failed to call _generateAddressForChain(0, $curIndex): $e\n$s", + level: LogLevel.Error); + } + + if (walletBase?.syncStatus is SyncedSyncStatus) { + refreshMutex = false; + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + coin, + ), + ); } } + + @override + Future send({ + required String toAddress, + required int amount, + Map args = const {}, + }) { + // not used for xmr + throw UnimplementedError(); + } + + @override + Future testNetworkConnection() async { + return await walletBase?.isConnected() ?? false; + } + + bool _isActive = false; + + @override + void Function(bool)? get onIsActiveWalletChanged => (isActive) async { + if (_isActive == isActive) { + return; + } + _isActive = isActive; + + if (isActive) { + _hasCalledExit = false; + String? password; + try { + password = + await keysStorage?.getWalletPassword(walletName: _walletId); + } catch (e, s) { + throw Exception("Password not found $e, $s"); + } + walletBase = (await walletService?.openWallet(_walletId, password!)) + as MoneroWalletBase?; + + walletBase!.onNewBlock = onNewBlock; + walletBase!.onNewTransaction = onNewTransaction; + walletBase!.syncStatusChanged = syncStatusChanged; + + if (!(await walletBase!.isConnected())) { + final node = await _getCurrentNode(); + final host = Uri.parse(node.host).host; + await walletBase?.connectToNode( + node: Node(uri: "$host:${node.port}", type: WalletType.monero)); + } + await walletBase?.startSync(); + await refresh(); + _autoSaveTimer?.cancel(); + _autoSaveTimer = Timer.periodic( + const Duration(seconds: 93), + (_) async => await walletBase?.save(), + ); + } else { + await exit(); + // _autoSaveTimer?.cancel(); + // await walletBase?.save(prioritySave: true); + // walletBase?.close(); + } + }; + + Future _updateCachedBalance(int sats) async { + await DB.instance.put( + boxName: walletId, + key: "cachedMoneroBalanceSats", + value: sats, + ); + } + + int _getCachedBalance() => + DB.instance.get( + boxName: walletId, + key: "cachedMoneroBalanceSats", + ) as int? ?? + 0; + + @override + Future get totalBalance async { + try { + final balanceEntries = walletBase?.balance?.entries; + if (balanceEntries != null) { + int bal = 0; + for (var element in balanceEntries) { + bal = bal + element.value.fullBalance; + } + await _updateCachedBalance(bal); + return Format.satoshisToAmount(bal, coin: coin); + } else { + final transactions = walletBase!.transactionHistory!.transactions; + int transactionBalance = 0; + for (var tx in transactions!.entries) { + if (tx.value.direction == TransactionDirection.incoming) { + transactionBalance += tx.value.amount!; + } else { + transactionBalance += -tx.value.amount! - tx.value.fee!; + } + } + + await _updateCachedBalance(transactionBalance); + return Format.satoshisToAmount(transactionBalance, coin: coin); + } + } catch (_) { + return Format.satoshisToAmount(_getCachedBalance(), coin: coin); + } + } + + @override + Future get transactionData => + _transactionData ??= _fetchTransactionData(); + + @override + // not used for xmr + Future> get unspentOutputs => throw UnimplementedError(); + + @override + Future updateNode(bool shouldRefresh) async { + final node = await _getCurrentNode(); + + final host = Uri.parse(node.host).host; + await walletBase?.connectToNode( + node: Node(uri: "$host:${node.port}", type: WalletType.monero)); + + // TODO: is this sync call needed? Do we need to notify ui here? + await walletBase?.startSync(); + + if (shouldRefresh) { + await refresh(); + } + } + + @override + Future updateSentCachedTxData(Map txData) async { + // not used for xmr + return; + } + + @override + bool validateAddress(String address) => walletBase!.validateAddress(address); + + @override + String get walletId => _walletId; + + /// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain] + /// and + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future _getCurrentAddressForChain(int chain) async { + // Here, we assume that chain == 1 if it isn't 0 + String arrayKey = chain == 0 ? "receivingAddresses" : "changeAddresses"; + final internalChainArray = (DB.instance + .get(boxName: walletId, key: arrayKey)) as List; + return internalChainArray.last as String; + } + + /// Increases the index for either the internal or external chain, depending on [chain]. + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future _incrementAddressIndexForChain(int chain) async { + // Here we assume chain == 1 if it isn't 0 + String indexKey = chain == 0 ? "receivingIndex" : "changeIndex"; + + final newIndex = + (DB.instance.get(boxName: walletId, key: indexKey)) + 1; + await DB.instance + .put(boxName: walletId, key: indexKey, value: newIndex); + } + + Future _generateAddressForChain(int chain, int index) async { + // + String address = walletBase!.getTransactionAddress(chain, index); + + return address; + } + + /// Adds [address] to the relevant chain's address array, which is determined by [chain]. + /// [address] - Expects a standard native segwit address + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future _addToAddressesArrayForChain(String address, int chain) async { + String chainArray = ''; + if (chain == 0) { + chainArray = 'receivingAddresses'; + } else { + chainArray = 'changeAddresses'; + } + + final addressArray = + DB.instance.get(boxName: walletId, key: chainArray); + if (addressArray == null) { + Logging.instance.log( + 'Attempting to add the following to $chainArray array for chain $chain:${[ + address + ]}', + level: LogLevel.Info); + await DB.instance + .put(boxName: walletId, key: chainArray, value: [address]); + } else { + // Make a deep copy of the existing list + final List newArray = []; + addressArray + .forEach((dynamic _address) => newArray.add(_address as String)); + newArray.add(address); // Add the address passed into the method + await DB.instance + .put(boxName: walletId, key: chainArray, value: newArray); + } + } + + Future _getFees() async { + // TODO: not use random hard coded values here + return FeeObject( + numberOfBlocksFast: 10, + numberOfBlocksAverage: 15, + numberOfBlocksSlow: 20, + fast: MoneroTransactionPriority.fast.raw!, + medium: MoneroTransactionPriority.regular.raw!, + slow: MoneroTransactionPriority.slow.raw!, + ); + } + + Future _fetchTransactionData() async { + await walletBase!.updateTransactions(); + final transactions = walletBase?.transactionHistory!.transactions; + + // final cachedTransactions = + // DB.instance.get(boxName: walletId, key: 'latest_tx_model') + // as TransactionData?; + // int latestTxnBlockHeight = + // DB.instance.get(boxName: walletId, key: "storedTxnDataHeight") + // as int? ?? + // 0; + + // final txidsList = DB.instance + // .get(boxName: walletId, key: "cachedTxids") as List? ?? + // []; + // + // final Set cachedTxids = Set.from(txidsList); + + // sort thing stuff + // change to get Monero price + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final List> midSortedArray = []; + + if (transactions != null) { + for (var tx in transactions.entries) { + // cachedTxids.add(tx.value.id); + Logging.instance.log( + "${tx.value.accountIndex} ${tx.value.addressIndex} ${tx.value.amount} ${tx.value.date} " + "${tx.value.direction} ${tx.value.fee} ${tx.value.height} ${tx.value.id} ${tx.value.isPending} ${tx.value.key} " + "${tx.value.recipientAddress}, ${tx.value.additionalInfo} con:${tx.value.confirmations}" + " ${tx.value.keyIndex}", + level: LogLevel.Info); + final worthNow = (currentPrice * + Format.satoshisToAmount( + tx.value.amount!, + coin: coin, + )) + .toStringAsFixed(2); + Map midSortedTx = {}; + // // create final tx map + midSortedTx["txid"] = tx.value.id; + midSortedTx["confirmed_status"] = !tx.value.isPending && + tx.value.confirmations! >= MINIMUM_CONFIRMATIONS; + midSortedTx["confirmations"] = tx.value.confirmations ?? 0; + midSortedTx["timestamp"] = + (tx.value.date.millisecondsSinceEpoch ~/ 1000); + midSortedTx["txType"] = + tx.value.direction == TransactionDirection.incoming + ? "Received" + : "Sent"; + midSortedTx["amount"] = tx.value.amount; + midSortedTx["worthNow"] = worthNow; + midSortedTx["worthAtBlockTimestamp"] = worthNow; + midSortedTx["fees"] = tx.value.fee; + if (tx.value.direction == TransactionDirection.incoming) { + final addressInfo = tx.value.additionalInfo; + + midSortedTx["address"] = walletBase?.getTransactionAddress( + addressInfo!['accountIndex'] as int, + addressInfo['addressIndex'] as int, + ); + } else { + midSortedTx["address"] = ""; + } + + final int txHeight = tx.value.height ?? 0; + midSortedTx["height"] = txHeight; + // if (txHeight >= latestTxnBlockHeight) { + // latestTxnBlockHeight = txHeight; + // } + + midSortedTx["aliens"] = []; + midSortedTx["inputSize"] = 0; + midSortedTx["outputSize"] = 0; + midSortedTx["inputs"] = []; + midSortedTx["outputs"] = []; + midSortedArray.add(midSortedTx); + } + } + + // sort by date ---- + midSortedArray + .sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int)); + Logging.instance.log(midSortedArray, level: LogLevel.Info); + + // buildDateTimeChunks + final Map result = {"dateTimeChunks": []}; + final dateArray = []; + + for (int i = 0; i < midSortedArray.length; i++) { + final txObject = midSortedArray[i]; + final date = extractDateFromTimestamp(txObject["timestamp"] as int); + final txTimeArray = [txObject["timestamp"], date]; + + if (dateArray.contains(txTimeArray[1])) { + result["dateTimeChunks"].forEach((dynamic chunk) { + if (extractDateFromTimestamp(chunk["timestamp"] as int) == + txTimeArray[1]) { + if (chunk["transactions"] == null) { + chunk["transactions"] = >[]; + } + chunk["transactions"].add(txObject); + } + }); + } else { + dateArray.add(txTimeArray[1]); + final chunk = { + "timestamp": txTimeArray[0], + "transactions": [txObject], + }; + result["dateTimeChunks"].add(chunk); + } + } + + // final transactionsMap = cachedTransactions?.getAllTransactions() ?? {}; + final Map transactionsMap = {}; + transactionsMap + .addAll(TransactionData.fromJson(result).getAllTransactions()); + + final txModel = TransactionData.fromMap(transactionsMap); + + // await DB.instance.put( + // boxName: walletId, + // key: 'storedTxnDataHeight', + // value: latestTxnBlockHeight); + // await DB.instance.put( + // boxName: walletId, key: 'latest_tx_model', value: txModel); + // await DB.instance.put( + // boxName: walletId, + // key: 'cachedTxids', + // value: cachedTxids.toList(growable: false)); + + return txModel; + } + + Future _pathForWalletDir({ + required String name, + required WalletType type, + }) async { + Directory root = await StackFileSystem.applicationRootDirectory(); + + final prefix = walletTypeToString(type).toLowerCase(); + final walletsDir = Directory('${root.path}/wallets'); + final walletDire = Directory('${walletsDir.path}/$prefix/$name'); + + if (!walletDire.existsSync()) { + walletDire.createSync(recursive: true); + } + + return walletDire.path; + } + + Future _pathForWallet({ + required String name, + required WalletType type, + }) async => + await _pathForWalletDir(name: name, type: type) + .then((path) => '$path/$name'); + + Future _getCurrentNode() async { + return NodeService(secureStorageInterface: _secureStorage) + .getPrimaryNodeFor(coin: coin) ?? + DefaultNodes.getNodeFor(coin); + } + + void onNewBlock() { + // + print("============================="); + print("New Block! :: $walletName"); + print("============================="); + } + + void onNewTransaction() { + // + print("============================="); + print("New Transaction! :: $walletName"); + print("============================="); + + // call this here? + GlobalEventBus.instance.fire( + UpdatedInBackgroundEvent( + "New data found in $walletId $walletName in background!", + walletId, + ), + ); + } + + void syncStatusChanged() async { + final syncStatus = walletBase?.syncStatus; + if (syncStatus != null) { + if (syncStatus.progress() == 1) { + refreshMutex = false; + } + + WalletSyncStatus? status; + _isConnected = true; + + if (syncStatus is SyncingSyncStatus) { + final int blocksLeft = syncStatus.blocksLeft; + + // ensure at least 1 to prevent math errors + final int height = max(1, syncStatus.height); + + final nodeHeight = height + blocksLeft; + + final percent = height / nodeHeight; + + final highest = max(highestPercentCached, percent); + + // update cached + if (highestPercentCached < percent) { + highestPercentCached = percent; + } + + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent( + highest, + walletId, + ), + ); + GlobalEventBus.instance.fire( + BlocksRemainingEvent( + blocksLeft, + walletId, + ), + ); + } else if (syncStatus is SyncedSyncStatus) { + status = WalletSyncStatus.synced; + } else if (syncStatus is NotConnectedSyncStatus) { + status = WalletSyncStatus.unableToSync; + _isConnected = false; + } else if (syncStatus is StartingSyncStatus) { + status = WalletSyncStatus.syncing; + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent( + highestPercentCached, + walletId, + ), + ); + } else if (syncStatus is FailedSyncStatus) { + status = WalletSyncStatus.unableToSync; + _isConnected = false; + } else if (syncStatus is ConnectingSyncStatus) { + status = WalletSyncStatus.syncing; + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent( + highestPercentCached, + walletId, + ), + ); + } else if (syncStatus is ConnectedSyncStatus) { + status = WalletSyncStatus.syncing; + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent( + highestPercentCached, + walletId, + ), + ); + } else if (syncStatus is LostConnectionSyncStatus) { + status = WalletSyncStatus.unableToSync; + _isConnected = false; + } + + if (status != null) { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + status, + walletId, + coin, + ), + ); + } + } + } + + Future _checkCurrentReceivingAddressesForTransactions() async { + try { + await _checkReceivingAddressForTransactions(); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future _checkReceivingAddressForTransactions() async { + try { + int highestIndex = -1; + for (var element + in walletBase!.transactionHistory!.transactions!.entries) { + if (element.value.direction == TransactionDirection.incoming) { + int curAddressIndex = + element.value.additionalInfo!['addressIndex'] as int; + if (curAddressIndex > highestIndex) { + highestIndex = curAddressIndex; + } + } + } + + // Check the new receiving index + String indexKey = "receivingIndex"; + final curIndex = + DB.instance.get(boxName: walletId, key: indexKey) as int; + if (highestIndex >= curIndex) { + // First increment the receiving index + await _incrementAddressIndexForChain(0); + final newReceivingIndex = + DB.instance.get(boxName: walletId, key: indexKey) as int; + + // Use new index to derive a new receiving address + final newReceivingAddress = + await _generateAddressForChain(0, newReceivingIndex); + + // Add that new receiving address to the array of receiving addresses + await _addToAddressesArrayForChain(newReceivingAddress, 0); + + _currentReceivingAddress = Future(() => newReceivingAddress); + } + } on SocketException catch (se, s) { + Logging.instance.log( + "SocketException caught in _checkReceivingAddressForTransactions(): $se\n$s", + level: LogLevel.Error); + return; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkReceivingAddressForTransactions(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + double get highestPercentCached => + DB.instance.get(boxName: walletId, key: "highestPercentCached") + as double? ?? + 0; + + set highestPercentCached(double value) => DB.instance.put( + boxName: walletId, + key: "highestPercentCached", + value: value, + ); } diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart index 72ba2e450..85fea61ad 100644 --- a/lib/services/coins/namecoin/namecoin_wallet.dart +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -1287,7 +1287,7 @@ class NamecoinWallet extends CoinServiceAPI { final priceData = await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; - final locale = await Devicelocale.currentLocale; + final locale = Platform.isWindows ? "en_US" : await Devicelocale.currentLocale; final String worthNow = Format.localizedStringAsFixed( value: ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 0249fb205..fe535dbf0 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -1200,7 +1200,7 @@ class ParticlWallet extends CoinServiceAPI { final priceData = await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; - final locale = await Devicelocale.currentLocale; + final locale = Platform.isWindows ? "en_US" : await Devicelocale.currentLocale; final String worthNow = Format.localizedStringAsFixed( value: ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / @@ -2241,8 +2241,10 @@ class ParticlWallet extends CoinServiceAPI { } } - Logging.instance.log("addAddresses: $allAddresses", level: LogLevel.Info); - Logging.instance.log("allTxHashes: $allTxHashes", level: LogLevel.Info); + Logging.instance.log("addAddresses: $allAddresses", + level: LogLevel.Info, printFullLength: true); + Logging.instance.log("allTxHashes: $allTxHashes", + level: LogLevel.Info, printFullLength: true); Logging.instance.log("allTransactions length: ${allTransactions.length}", level: LogLevel.Info); @@ -2286,7 +2288,7 @@ class ParticlWallet extends CoinServiceAPI { for (final out in tx["vout"] as List) { if (prevOut == out["n"]) { - final address = out["scriptPubKey"]["address"] as String?; + final address = out["scriptPubKey"]["addresses"][0] as String?; if (address != null) { sendersArray.add(address); } @@ -2300,7 +2302,7 @@ class ParticlWallet extends CoinServiceAPI { // Particl has different tx types that need to be detected and handled here if (output.containsKey('scriptPubKey') as bool) { // Logging.instance.log("output is transparent", level: LogLevel.Info); - final address = output["scriptPubKey"]["address"] as String?; + final address = output["scriptPubKey"]["addresses"][0] as String?; if (address != null) { recipientsArray.add(address); } @@ -2356,20 +2358,26 @@ class ParticlWallet extends CoinServiceAPI { for (final output in txObject["vout"] as List) { // Particl has different tx types that need to be detected and handled here if (output.containsKey('scriptPubKey') as bool) { - // Logging.instance.log("output is transparent", level: LogLevel.Info); - final String address = output["scriptPubKey"]!["address"] as String; - final value = output["value"]!; - final _value = (Decimal.parse(value.toString()) * - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toBigInt() - .toInt(); - totalOutput += _value; - if (changeAddresses.contains(address)) { - inputAmtSentFromWallet -= _value; - } else { - // change address from 'sent from' to the 'sent to' address - txObject["address"] = address; + try { + final String address = + output["scriptPubKey"]!["addresses"][0] as String; + final value = output["value"]!; + final _value = (Decimal.parse(value.toString()) * + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toBigInt() + .toInt(); + totalOutput += _value; + if (changeAddresses.contains(address)) { + inputAmtSentFromWallet -= _value; + } else { + // change address from 'sent from' to the 'sent to' address + txObject["address"] = address; + } + } catch (s, e) { + Logging.instance.log(s.toString(), level: LogLevel.Warning); } + // Logging.instance.log("output is transparent", level: LogLevel.Info); + } else if (output.containsKey('ct_fee') as bool) { // or type: data // TODO handle CT tx @@ -2408,16 +2416,20 @@ class ParticlWallet extends CoinServiceAPI { // add up received tx value for (final output in txObject["vout"] as List) { - final address = output["scriptPubKey"]["address"]; - if (address != null) { - final value = (Decimal.parse(output["value"].toString()) * - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toBigInt() - .toInt(); - totalOut += value; - if (allAddresses.contains(address)) { - outputAmtAddressedToWallet += value; + try { + final address = output["scriptPubKey"]["addresses"][0]; + if (address != null) { + final value = (Decimal.parse(output["value"].toString()) * + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toBigInt() + .toInt(); + totalOut += value; + if (allAddresses.contains(address)) { + outputAmtAddressedToWallet += value; + } } + } catch (s, e) { + Logging.instance.log(s.toString(), level: LogLevel.Info); } } @@ -2574,7 +2586,6 @@ class ParticlWallet extends CoinServiceAPI { final List availableOutputs = utxos ?? outputsList; final List spendableOutputs = []; int spendableSatoshiValue = 0; - print("AVAILABLE UTXOS IS ::::: ${availableOutputs}"); // Build list of spendable outputs and totaling their satoshi amount for (var i = 0; i < availableOutputs.length; i++) { if (availableOutputs[i].blocked == false && @@ -2907,8 +2918,6 @@ class ParticlWallet extends CoinServiceAPI { Map results = {}; Map> addressTxid = {}; - print("CALLING FETCH BUILD TX DATA"); - // addresses to check List addressesP2PKH = []; List addressesP2WPKH = []; @@ -2925,8 +2934,7 @@ class ParticlWallet extends CoinServiceAPI { for (final output in tx["vout"] as List) { final n = output["n"]; if (n != null && n == utxosToUse[i].vout) { - print("SCRIPT PUB KEY IS ${output["scriptPubKey"]}"); - final address = output["scriptPubKey"]["address"] as String; + final address = output["scriptPubKey"]["addresses"][0] as String; if (!addressTxid.containsKey(address)) { addressTxid[address] = []; } @@ -3088,6 +3096,7 @@ class ParticlWallet extends CoinServiceAPI { // Add transaction inputs for (var i = 0; i < utxosToUse.length; i++) { final txid = utxosToUse[i].txid; + txb.addInput(txid, utxosToUse[i].vout, null, utxoSigningData[txid]["output"] as Uint8List, ''); } diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index 9e152d9e6..d21ebab8c 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/node.dart'; @@ -16,7 +17,6 @@ import 'package:cw_wownero/api/wallet.dart'; import 'package:cw_wownero/pending_wownero_transaction.dart'; import 'package:cw_wownero/wownero_amount_format.dart'; import 'package:cw_wownero/wownero_wallet.dart'; -import 'package:dart_numerics/dart_numerics.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_libmonero/core/key_service.dart'; @@ -33,7 +33,6 @@ import 'package:stackwallet/models/paymint/transactions_model.dart'; import 'package:stackwallet/models/paymint/utxo_model.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/event_bus/events/global/blocks_remaining_event.dart'; -import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; @@ -52,1347 +51,46 @@ import 'package:stackwallet/utilities/stack_file_system.dart'; const int MINIMUM_CONFIRMATIONS = 10; -//https://github.com/wownero-project/wownero/blob/8361d60aef6e17908658128284899e3a11d808d4/src/cryptonote_config.h#L162 -const String GENESIS_HASH_MAINNET = - "013c01ff0001ffffffffffff03029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1"; -const String GENESIS_HASH_TESTNET = - "013c01ff0001ffffffffffff03029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1"; - class WowneroWallet extends CoinServiceAPI { - static const integrationTestFlag = - bool.fromEnvironment("IS_INTEGRATION_TEST"); - final _prefs = Prefs.instance; - - Timer? timer; - Timer? wowneroAutosaveTimer; - late Coin _coin; - - late SecureStorageInterface _secureStore; - - late PriceAPI _priceAPI; - - Future getCurrentNode() async { - return NodeService(secureStorageInterface: _secureStore) - .getPrimaryNodeFor(coin: coin) ?? - DefaultNodes.getNodeFor(coin); - } - - WowneroWallet( - {required String walletId, - required String walletName, - required Coin coin, - PriceAPI? priceAPI, - required SecureStorageInterface secureStore}) { - _walletId = walletId; - _walletName = walletName; - _coin = coin; - - _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = secureStore; - } + final String _walletId; + final Coin _coin; + final PriceAPI _priceAPI; + final SecureStorageInterface _secureStorage; + final Prefs _prefs; + String _walletName; bool _shouldAutoSync = false; - - @override - bool get shouldAutoSync => _shouldAutoSync; - - @override - set shouldAutoSync(bool shouldAutoSync) { - if (_shouldAutoSync != shouldAutoSync) { - _shouldAutoSync = shouldAutoSync; - if (!shouldAutoSync) { - timer?.cancel(); - wowneroAutosaveTimer?.cancel(); - timer = null; - wowneroAutosaveTimer = null; - stopNetworkAlivePinging(); - } else { - startNetworkAlivePinging(); - // Walletbase needs to be open for this to work - refresh(); - } - } - } - - @override - Future updateNode(bool shouldRefresh) async { - final node = await getCurrentNode(); - - final host = Uri.parse(node.host).host; - await walletBase?.connectToNode( - node: Node(uri: "$host:${node.port}", type: WalletType.wownero)); - - // TODO: is this sync call needed? Do we need to notify ui here? - await walletBase?.startSync(); - - if (shouldRefresh) { - await refresh(); - } - } - - Future> _getMnemonicList() async { - final mnemonicString = - await _secureStore.read(key: '${_walletId}_mnemonic'); - if (mnemonicString == null) { - return []; - } - final List data = mnemonicString.split(' '); - return data; - } - - @override - Future> get mnemonic => _getMnemonicList(); - - Future get currentNodeHeight async { - try { - if (walletBase!.syncStatus! is SyncedSyncStatus && - walletBase!.syncStatus!.progress() == 1.0) { - return await walletBase!.getNodeHeight(); - } - } catch (e, s) {} - int _height = -1; - try { - _height = (walletBase!.syncStatus as SyncingSyncStatus).height; - } catch (e, s) { - // Logging.instance.log("$e $s", level: LogLevel.Warning); - } - - int blocksRemaining = -1; - - try { - blocksRemaining = - (walletBase!.syncStatus as SyncingSyncStatus).blocksLeft; - } catch (e, s) { - // Logging.instance.log("$e $s", level: LogLevel.Warning); - } - int currentHeight = _height + blocksRemaining; - if (_height == -1 || blocksRemaining == -1) { - currentHeight = int64MaxValue; - } - final cachedHeight = DB.instance - .get(boxName: walletId, key: "storedNodeHeight") as int? ?? - 0; - - if (currentHeight > cachedHeight && currentHeight != int64MaxValue) { - await DB.instance.put( - boxName: walletId, key: "storedNodeHeight", value: currentHeight); - return currentHeight; - } else { - return cachedHeight; - } - } - - Future get currentSyncingHeight async { - //TODO return the tip of the wownero blockchain - try { - if (walletBase!.syncStatus! is SyncedSyncStatus && - walletBase!.syncStatus!.progress() == 1.0) { - // Logging.instance - // .log("currentSyncingHeight lol", level: LogLevel.Warning); - return getSyncingHeight(); - } - } catch (e, s) {} - int syncingHeight = -1; - try { - syncingHeight = (walletBase!.syncStatus as SyncingSyncStatus).height; - } catch (e, s) { - // Logging.instance.log("$e $s", level: LogLevel.Warning); - } - final cachedHeight = - DB.instance.get(boxName: walletId, key: "storedSyncingHeight") - as int? ?? - 0; - - if (syncingHeight > cachedHeight) { - await DB.instance.put( - boxName: walletId, key: "storedSyncingHeight", value: syncingHeight); - return syncingHeight; - } else { - return cachedHeight; - } - } - - Future updateStoredChainHeight({required int newHeight}) async { - await DB.instance.put( - boxName: walletId, key: "storedChainHeight", value: newHeight); - } - - int get storedChainHeight { - return DB.instance.get(boxName: walletId, key: "storedChainHeight") - as int? ?? - 0; - } - - /// Increases the index for either the internal or external chain, depending on [chain]. - /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! - Future _incrementAddressIndexForChain(int chain) async { - // Here we assume chain == 1 if it isn't 0 - String indexKey = chain == 0 ? "receivingIndex" : "changeIndex"; - - final newIndex = - (DB.instance.get(boxName: walletId, key: indexKey)) + 1; - await DB.instance - .put(boxName: walletId, key: indexKey, value: newIndex); - } - - Future _checkCurrentReceivingAddressesForTransactions() async { - try { - await _checkReceivingAddressForTransactions(); - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s", - level: LogLevel.Error); - rethrow; - } - } - - Future _checkReceivingAddressForTransactions() async { - try { - int highestIndex = -1; - for (var element - in walletBase!.transactionHistory!.transactions!.entries) { - if (element.value.direction == TransactionDirection.incoming) { - int curAddressIndex = - element.value.additionalInfo!['addressIndex'] as int; - if (curAddressIndex > highestIndex) { - highestIndex = curAddressIndex; - } - } - } - - // Check the new receiving index - String indexKey = "receivingIndex"; - final curIndex = - DB.instance.get(boxName: walletId, key: indexKey) as int; - if (highestIndex >= curIndex) { - // First increment the receiving index - await _incrementAddressIndexForChain(0); - final newReceivingIndex = - DB.instance.get(boxName: walletId, key: indexKey) as int; - - // Use new index to derive a new receiving address - final newReceivingAddress = - await _generateAddressForChain(0, newReceivingIndex); - - // Add that new receiving address to the array of receiving addresses - await _addToAddressesArrayForChain(newReceivingAddress, 0); - - // Set the new receiving address that the service - - _currentReceivingAddress = Future(() => newReceivingAddress); - } - } on SocketException catch (se, s) { - Logging.instance.log( - "SocketException caught in _checkReceivingAddressForTransactions(): $se\n$s", - level: LogLevel.Error); - return; - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from _checkReceivingAddressForTransactions(): $e\n$s", - level: LogLevel.Error); - rethrow; - } - } - - @override - bool get isRefreshing => refreshMutex; - - bool refreshMutex = false; - - Timer? syncPercentTimer; - - Mutex syncHeightMutex = Mutex(); - Future stopSyncPercentTimer() async { - syncPercentTimer?.cancel(); - syncPercentTimer = null; - } - - Future startSyncPercentTimer() async { - if (syncPercentTimer != null) { - return; - } - syncPercentTimer?.cancel(); - GlobalEventBus.instance - .fire(RefreshPercentChangedEvent(highestPercentCached, walletId)); - syncPercentTimer = Timer.periodic(const Duration(seconds: 30), (_) async { - if (syncHeightMutex.isLocked) { - return; - } - await syncHeightMutex.protect(() async { - // int restoreheight = walletBase!.walletInfo.restoreHeight ?? 0; - int _height = await currentSyncingHeight; - int _currentHeight = await currentNodeHeight; - double progress = 0; - try { - progress = walletBase!.syncStatus!.progress(); - } catch (e, s) { - Logging.instance.log("$e $s", level: LogLevel.Warning); - } - - final int blocksRemaining = _currentHeight - _height; - - GlobalEventBus.instance - .fire(BlocksRemainingEvent(blocksRemaining, walletId)); - - if (progress == 1 && _currentHeight > 0 && _height > 0) { - await stopSyncPercentTimer(); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - coin, - ), - ); - return; - } - - // for some reason this can be 0 which screws up the percent calculation - // int64MaxValue is NOT the best value to use here - if (_currentHeight < 1) { - _currentHeight = int64MaxValue; - } - - if (_height < 1) { - _height = 1; - } - - double restorePercent = progress; - double highestPercent = highestPercentCached; - - Logging.instance.log( - "currentSyncingHeight: $_height, nodeHeight: $_currentHeight, restorePercent: $restorePercent, highestPercentCached: $highestPercentCached", - level: LogLevel.Info); - - if (restorePercent > 0 && restorePercent <= 1) { - // if (restorePercent > highestPercent) { - highestPercent = restorePercent; - highestPercentCached = restorePercent; - // } - } - - GlobalEventBus.instance - .fire(RefreshPercentChangedEvent(highestPercent, walletId)); - }); - }); - } - - double get highestPercentCached => - DB.instance.get(boxName: walletId, key: "highestPercentCached") - as double? ?? - 0; - set highestPercentCached(double value) => DB.instance.put( - boxName: walletId, - key: "highestPercentCached", - value: value, - ); - - /// Refreshes display data for the wallet - @override - Future refresh() async { - if (refreshMutex) { - Logging.instance.log("$walletId $walletName refreshMutex denied", - level: LogLevel.Info); - return; - } else { - refreshMutex = true; - } - - if (walletBase == null) { - throw Exception("Tried to call refresh() in wownero without walletBase!"); - } - - try { - await startSyncPercentTimer(); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - - final int _currentSyncingHeight = await currentSyncingHeight; - final int storedHeight = storedChainHeight; - int _currentNodeHeight = await currentNodeHeight; - - double progress = 0; - try { - progress = (walletBase!.syncStatus!).progress(); - } catch (e, s) { - // Logging.instance.log("$e $s", level: LogLevel.Warning); - } - await _fetchTransactionData(); - - bool stillSyncing = false; - Logging.instance.log( - "storedHeight: $storedHeight, _currentSyncingHeight: $_currentSyncingHeight, _currentNodeHeight: $_currentNodeHeight, progress: $progress, issynced: ${await walletBase!.isConnected()}", - level: LogLevel.Info); - - if (progress < 1.0) { - stillSyncing = true; - } - - if (_currentSyncingHeight > storedHeight) { - // 0 is returned from wownero as I assume an error????? - if (_currentSyncingHeight > 0) { - // 0 failed to fetch current height??? - await updateStoredChainHeight(newHeight: _currentSyncingHeight); - } - } - - await _checkCurrentReceivingAddressesForTransactions(); - String indexKey = "receivingIndex"; - final curIndex = - DB.instance.get(boxName: walletId, key: indexKey) as int; - // Use new index to derive a new receiving address - try { - final newReceivingAddress = await _generateAddressForChain(0, curIndex); - _currentReceivingAddress = Future(() => newReceivingAddress); - } catch (e, s) { - Logging.instance.log( - "Failed to call _generateAddressForChain(0, $curIndex): $e\n$s", - level: LogLevel.Error); - } - final newTxData = await _fetchTransactionData(); - _transactionData = Future(() => newTxData); - - if (isActive || shouldAutoSync) { - timer ??= Timer.periodic(const Duration(seconds: 60), (timer) async { - //todo: check if print needed - // debugPrint("run timer"); - //TODO: check for new data and refresh if needed. if wownero even needs this - // chain height check currently broken - // if ((await chainHeight) != (await storedChainHeight)) { - // if (await refreshIfThereIsNewData()) { - await refresh(); - GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( - "New data found in $walletId $walletName in background!", - walletId)); - // } - // } - }); - wowneroAutosaveTimer ??= - Timer.periodic(const Duration(seconds: 93), (timer) async { - //todo: check if print needed - // debugPrint("run wownero timer"); - if (isActive) { - await walletBase?.save(); - GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( - "New data found in $walletId $walletName in background!", - walletId)); - } - }); - } - - if (stillSyncing) { - debugPrint("still syncing"); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - refreshMutex = false; - return; - } - await stopSyncPercentTimer(); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - coin, - ), - ); - refreshMutex = false; - } catch (error, strace) { - refreshMutex = false; - await stopSyncPercentTimer(); - GlobalEventBus.instance.fire( - NodeConnectionStatusChangedEvent( - NodeConnectionStatus.disconnected, - walletId, - coin, - ), - ); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.unableToSync, - walletId, - coin, - ), - ); - Logging.instance.log( - "Caught exception in refreshWalletData(): $error\n$strace", - level: LogLevel.Error); - } - } - - @override - // TODO: implement allOwnAddresses - Future> get allOwnAddresses { - return Future(() => []); - } - - @override - Future get balanceMinusMaxFee async => - (await availableBalance) - - Format.satoshisToAmount(await maxFee, coin: Coin.wownero); - - @override - Future get currentReceivingAddress => - _currentReceivingAddress ??= _getCurrentAddressForChain(0); - - @override - Future exit() async { - await stopSyncPercentTimer(); - _hasCalledExit = true; - isActive = false; - await walletBase?.save(prioritySave: true); - walletBase?.close(); - wowneroAutosaveTimer?.cancel(); - wowneroAutosaveTimer = null; - timer?.cancel(); - timer = null; - stopNetworkAlivePinging(); - } - + bool _isConnected = false; bool _hasCalledExit = false; - - @override - bool get hasCalledExit => _hasCalledExit; - - Future? _currentReceivingAddress; - - Future _getFees() async { - // TODO: not use random hard coded values here - return FeeObject( - numberOfBlocksFast: 10, - numberOfBlocksAverage: 15, - numberOfBlocksSlow: 20, - fast: MoneroTransactionPriority.fast.raw!, - medium: MoneroTransactionPriority.regular.raw!, - slow: MoneroTransactionPriority.slow.raw!, - ); - } - - @override - Future get fees => _feeObject ??= _getFees(); - Future? _feeObject; - - @override - // TODO: implement fullRescan - Future fullRescan( - int maxUnusedAddressGap, - int maxNumberOfIndexesToCheck, - ) async { - var restoreHeight = walletBase?.walletInfo.restoreHeight; - await walletBase?.rescan(height: restoreHeight); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - return; - } - - Future _generateAddressForChain(int chain, int index) async { - // - String address = walletBase!.getTransactionAddress(chain, index); - - return address; - } - - /// Adds [address] to the relevant chain's address array, which is determined by [chain]. - /// [address] - Expects a standard native segwit address - /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! - Future _addToAddressesArrayForChain(String address, int chain) async { - String chainArray = ''; - if (chain == 0) { - chainArray = 'receivingAddresses'; - } else { - chainArray = 'changeAddresses'; - } - - final addressArray = - DB.instance.get(boxName: walletId, key: chainArray); - if (addressArray == null) { - Logging.instance.log( - 'Attempting to add the following to $chainArray array for chain $chain:${[ - address - ]}', - level: LogLevel.Info); - await DB.instance - .put(boxName: walletId, key: chainArray, value: [address]); - } else { - // Make a deep copy of the existing list - final List newArray = []; - addressArray - .forEach((dynamic _address) => newArray.add(_address as String)); - newArray.add(address); // Add the address passed into the method - await DB.instance - .put(boxName: walletId, key: chainArray, value: newArray); - } - } - - /// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain] - /// and - /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! - Future _getCurrentAddressForChain(int chain) async { - // Here, we assume that chain == 1 if it isn't 0 - String arrayKey = chain == 0 ? "receivingAddresses" : "changeAddresses"; - final internalChainArray = (DB.instance - .get(boxName: walletId, key: arrayKey)) as List; - return internalChainArray.last as String; - } - - //TODO: take in the default language when creating wallet. - Future _generateNewWallet({int seedWordsLength = 14}) async { - Logging.instance - .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); - // TODO: ping wownero server and make sure the genesis hash matches - // if (!integrationTestFlag) { - // final features = await electrumXClient.getServerFeatures(); - // Logging.instance.log("features: $features"); - // if (_networkType == BasicNetworkType.main) { - // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { - // throw Exception("genesis hash does not match main net!"); - // } - // } else if (_networkType == BasicNetworkType.test) { - // if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { - // throw Exception("genesis hash does not match test net!"); - // } - // } - // } - - // this should never fail - if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) { - throw Exception( - "Attempted to overwrite mnemonic on generate new wallet!"); - } - - // TODO: Wallet Service may need to be switched to Wownero - walletService = - wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); - keysStorage = KeyService(_secureStore); - WalletInfo walletInfo; - WalletCredentials credentials; - try { - String name = _walletId; - final dirPath = - await pathForWalletDir(name: name, type: WalletType.wownero); - final path = await pathForWallet(name: name, type: WalletType.wownero); - credentials = wownero.createWowneroNewWalletCredentials( - name: name, language: "English", seedWordsLength: seedWordsLength); - - walletInfo = WalletInfo.external( - id: WalletBase.idFor(name, WalletType.wownero), - name: name, - type: WalletType.wownero, - isRecovery: false, - restoreHeight: credentials.height ?? 0, - date: DateTime.now(), - path: path, - dirPath: dirPath, - // TODO: find out what to put for address - address: ''); - credentials.walletInfo = walletInfo; - - _walletCreationService = WalletCreationService( - secureStorage: _secureStore, - walletService: walletService, - keyService: keysStorage, - ); - _walletCreationService?.changeWalletType(); - // To restore from a seed - final wallet = await _walletCreationService?.create(credentials); - - final bufferedCreateHeight = (seedWordsLength == 14) - ? getSeedHeightSync(wallet?.seed.trim() as String) - : wownero.getHeightByDate( - date: DateTime.now().subtract(const Duration( - days: - 2))); // subtract a couple days to ensure we have a buffer for SWB - - await DB.instance.put( - boxName: walletId, key: "restoreHeight", value: bufferedCreateHeight); - walletInfo.restoreHeight = bufferedCreateHeight; - - await _secureStore.write( - key: '${_walletId}_mnemonic', value: wallet?.seed.trim()); - - walletInfo.address = wallet?.walletAddresses.address; - await DB.instance - .add(boxName: WalletInfo.boxName, value: walletInfo); - walletBase?.close(); - walletBase = wallet as WowneroWalletBase; - } catch (e, s) { - debugPrint(e.toString()); - debugPrint(s.toString()); - } - final node = await getCurrentNode(); - final host = Uri.parse(node.host).host; - await walletBase?.connectToNode( - node: Node(uri: "$host:${node.port}", type: WalletType.wownero)); - await walletBase?.startSync(); - await DB.instance - .put(boxName: walletId, key: "id", value: _walletId); - - // Set relevant indexes - await DB.instance - .put(boxName: walletId, key: "receivingIndex", value: 0); - await DB.instance - .put(boxName: walletId, key: "changeIndex", value: 0); - await DB.instance.put( - boxName: walletId, - key: 'blocked_tx_hashes', - value: ["0xdefault"], - ); // A list of transaction hashes to represent frozen utxos in wallet - // initialize address book entries - await DB.instance.put( - boxName: walletId, - key: 'addressBookEntries', - value: {}); - await DB.instance - .put(boxName: walletId, key: "isFavorite", value: false); - - // Generate and add addresses to relevant arrays - final initialReceivingAddress = await _generateAddressForChain(0, 0); - // final initialChangeAddress = await _generateAddressForChain(1, 0); - - await _addToAddressesArrayForChain(initialReceivingAddress, 0); - // await _addToAddressesArrayForChain(initialChangeAddress, 1); - - await DB.instance.put( - boxName: walletId, - key: 'receivingAddresses', - value: [initialReceivingAddress]); - await DB.instance - .put(boxName: walletId, key: "receivingIndex", value: 0); - - _currentReceivingAddress = Future(() => initialReceivingAddress); - - Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info); - } - - @override - // TODO: implement initializeWallet - Future initializeNew({int seedWordsLength = 14}) async { - await _prefs.init(); - // TODO: ping actual wownero network - // try { - // final hasNetwork = await _electrumXClient.ping(); - // if (!hasNetwork) { - // return false; - // } - // } catch (e, s) { - // Logging.instance.log("Caught in initializeWallet(): $e\n$s"); - // return false; - // } - walletService = - wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); - keysStorage = KeyService(_secureStore); - - await _generateNewWallet(seedWordsLength: seedWordsLength); - // var password; - // try { - // password = - // await keysStorage?.getWalletPassword(walletName: this._walletId); - // } catch (e, s) { - // Logging.instance.log("$e $s"); - // Logging.instance.log("Generating new ${coin.ticker} wallet."); - // // Triggers for new users automatically. Generates new wallet - // await _generateNewWallet(wallet); - // await wallet.put("id", this._walletId); - // return true; - // } - // walletBase = (await walletService?.openWallet(this._walletId, password)) - // as WowneroWalletBase; - // Logging.instance.log("Opening existing ${coin.ticker} wallet."); - // // Wallet already exists, triggers for a returning user - // final currentAddress = awaicurrentHeightt _getCurrentAddressForChain(0); - // this._currentReceivingAddress = Future(() => currentAddress); - // - // await walletBase?.connectToNode( - // node: Node( - // uri: "xmr-node.cakewallet.com:18081", type: WalletType.wownero)); - // walletBase?.startSync(); - - return true; - } - - @override - Future initializeExisting() async { - Logging.instance.log( - "Opening existing ${coin.prettyName} wallet $walletName...", - level: LogLevel.Info); - - if ((DB.instance.get(boxName: walletId, key: "id")) == null) { - //todo: check if print needed - // debugPrint("Exception was thrown"); - throw Exception( - "Attempted to initialize an existing wallet using an unknown wallet ID!"); - } - - walletService = - wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); - keysStorage = KeyService(_secureStore); - - await _prefs.init(); - final data = - DB.instance.get(boxName: walletId, key: "latest_tx_model") - as TransactionData?; - if (data != null) { - _transactionData = Future(() => data); - } - - String? password; - try { - password = await keysStorage?.getWalletPassword(walletName: _walletId); - } catch (e, s) { - //todo: check if print needed - // debugPrint("Exception was thrown $e $s"); - throw Exception("Password not found $e, $s"); - } - walletBase = (await walletService?.openWallet(_walletId, password!)) - as WowneroWalletBase; - debugPrint("walletBase $walletBase"); - Logging.instance.log( - "Opened existing ${coin.prettyName} wallet $walletName", - level: LogLevel.Info); - // Wallet already exists, triggers for a returning user - - String indexKey = "receivingIndex"; - final curIndex = - await DB.instance.get(boxName: walletId, key: indexKey) as int; - // Use new index to derive a new receiving address - final newReceivingAddress = await _generateAddressForChain(0, curIndex); - Logging.instance.log( - "wownero address in init existing: $newReceivingAddress", - level: LogLevel.Info); - _currentReceivingAddress = Future(() => newReceivingAddress); - } - - @override - Future get maxFee async { - var bal = await availableBalance; - var fee = walletBase!.calculateEstimatedFee( - wownero.getDefaultTransactionPriority(), - Format.decimalAmountToSatoshis(bal, coin), - ); - - return fee; - } - - @override - // TODO: implement pendingBalance - Future get pendingBalance => throw UnimplementedError(); - + bool refreshMutex = false; bool longMutex = false; - // TODO: are these needed? - WalletService? walletService; KeyService? keysStorage; WowneroWalletBase? walletBase; WalletCreationService? _walletCreationService; + Timer? _autoSaveTimer; - String toStringForinfo(WalletInfo info) { - return "id: ${info.id} name: ${info.name} type: ${info.type} recovery: ${info.isRecovery}" - " restoreheight: ${info.restoreHeight} timestamp: ${info.timestamp} dirPath: ${info.dirPath} " - "path: ${info.path} address: ${info.address} addresses: ${info.addresses}"; - } - - Future pathForWalletDir({ - required String name, - required WalletType type, - }) async { - Directory root = await StackFileSystem.applicationRootDirectory(); - - final prefix = walletTypeToString(type).toLowerCase(); - final walletsDir = Directory('${root.path}/wallets'); - final walletDire = Directory('${walletsDir.path}/$prefix/$name'); - - if (!walletDire.existsSync()) { - walletDire.createSync(recursive: true); - } - - return walletDire.path; - } - - Future pathForWallet({ - required String name, - required WalletType type, - }) async => - await pathForWalletDir(name: name, type: type) - .then((path) => '$path/$name'); - - // TODO: take in a dynamic height - @override - Future recoverFromMnemonic({ - required String mnemonic, - required int maxUnusedAddressGap, - required int maxNumberOfIndexesToCheck, - required int height, - }) async { - final int seedLength = mnemonic.trim().split(" ").length; - if (!(seedLength == 14 || seedLength == 25)) { - throw Exception("Invalid wownero mnemonic length found: $seedLength"); - } - - await _prefs.init(); - longMutex = true; - final start = DateTime.now(); - try { - // Logging.instance.log("IS_INTEGRATION_TEST: $integrationTestFlag"); - // if (!integrationTestFlag) { - // final features = await electrumXClient.getServerFeatures(); - // Logging.instance.log("features: $features"); - // if (_networkType == BasicNetworkType.main) { - // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { - // throw Exception("genesis hash does not match main net!"); - // } - // } else if (_networkType == BasicNetworkType.test) { - // if (features['genesis_hash'] != GENESIS_HASH_TESTNET) { - // throw Exception("genesis hash does not match test net!"); - // } - // } - // } - // check to make sure we aren't overwriting a mnemonic - // this should never fail - if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) { - longMutex = false; - throw Exception("Attempted to overwrite mnemonic on restore!"); - } - await _secureStore.write( - key: '${_walletId}_mnemonic', value: mnemonic.trim()); - - // extract seed height from 14 word seed - if (seedLength == 14) { - height = getSeedHeightSync(mnemonic.trim()); - } else { - // 25 word seed. TODO validate - if (height == 0) { - height = wownero.getHeightByDate( - date: DateTime.now().subtract(const Duration( - days: - 2))); // subtract a couple days to ensure we have a buffer for SWB\ - } - } - - await DB.instance - .put(boxName: walletId, key: "restoreHeight", value: height); - - walletService = - wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); - keysStorage = KeyService(_secureStore); - WalletInfo walletInfo; - WalletCredentials credentials; - String name = _walletId; - final dirPath = - await pathForWalletDir(name: name, type: WalletType.wownero); - final path = await pathForWallet(name: name, type: WalletType.wownero); - credentials = wownero.createWowneroRestoreWalletFromSeedCredentials( - name: name, - height: height, - mnemonic: mnemonic.trim(), - ); - try { - walletInfo = WalletInfo.external( - id: WalletBase.idFor(name, WalletType.wownero), - name: name, - type: WalletType.wownero, - isRecovery: false, - restoreHeight: credentials.height ?? 0, - date: DateTime.now(), - path: path, - dirPath: dirPath, - // TODO: find out what to put for address - address: ''); - credentials.walletInfo = walletInfo; - - _walletCreationService = WalletCreationService( - secureStorage: _secureStore, - walletService: walletService, - keyService: keysStorage, - ); - _walletCreationService!.changeWalletType(); - // To restore from a seed - final wallet = - await _walletCreationService!.restoreFromSeed(credentials); - walletInfo.address = wallet.walletAddresses.address; - await DB.instance - .add(boxName: WalletInfo.boxName, value: walletInfo); - walletBase?.close(); - walletBase = wallet as WowneroWalletBase; - await DB.instance.put( - boxName: walletId, - key: 'receivingAddresses', - value: [walletInfo.address!]); - await DB.instance - .put(boxName: walletId, key: "receivingIndex", value: 0); - await DB.instance - .put(boxName: walletId, key: "id", value: _walletId); - await DB.instance - .put(boxName: walletId, key: "changeIndex", value: 0); - await DB.instance.put( - boxName: walletId, - key: 'blocked_tx_hashes', - value: ["0xdefault"], - ); // A list of transaction hashes to represent frozen utxos in wallet - // initialize address book entries - await DB.instance.put( - boxName: walletId, - key: 'addressBookEntries', - value: {}); - await DB.instance - .put(boxName: walletId, key: "isFavorite", value: false); - } catch (e, s) { - //todo: come back to this - debugPrint(e.toString()); - debugPrint(s.toString()); - } - final node = await getCurrentNode(); - final host = Uri.parse(node.host).host; - await walletBase?.connectToNode( - node: Node(uri: "$host:${node.port}", type: WalletType.wownero)); - await walletBase?.rescan(height: credentials.height); - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from recoverFromMnemonic(): $e\n$s", - level: LogLevel.Error); - longMutex = false; - rethrow; - } - longMutex = false; - - final end = DateTime.now(); - Logging.instance.log( - "$walletName Recovery time: ${end.difference(start).inMilliseconds} millis", - level: LogLevel.Info); - } - - @override - Future send({ - required String toAddress, - required int amount, - Map args = const {}, - }) async { - try { - final txData = await prepareSend( - address: toAddress, satoshiAmount: amount, args: args); - final txHash = await confirmSend(txData: txData); - return txHash; - } catch (e, s) { - Logging.instance - .log("Exception rethrown from send(): $e\n$s", level: LogLevel.Error); - rethrow; - } - } - - @override - Future testNetworkConnection() async { - return await walletBase?.isConnected() ?? false; - } - - Timer? _networkAliveTimer; - - void startNetworkAlivePinging() { - // call once on start right away - _periodicPingCheck(); - - // then periodically check - _networkAliveTimer = Timer.periodic( - Constants.networkAliveTimerDuration, - (_) async { - _periodicPingCheck(); - }, - ); - } - - void _periodicPingCheck() async { - bool hasNetwork = await testNetworkConnection(); - _isConnected = hasNetwork; - if (_isConnected != hasNetwork) { - NodeConnectionStatus status = hasNetwork - ? NodeConnectionStatus.connected - : NodeConnectionStatus.disconnected; - GlobalEventBus.instance - .fire(NodeConnectionStatusChangedEvent(status, walletId, coin)); - } - } - - void stopNetworkAlivePinging() { - _networkAliveTimer?.cancel(); - _networkAliveTimer = null; - } - - bool _isConnected = false; - - @override - bool get isConnected => _isConnected; - - @override - Future get totalBalance async { - var transactions = walletBase?.transactionHistory!.transactions; - int transactionBalance = 0; - for (var tx in transactions!.entries) { - if (tx.value.direction == TransactionDirection.incoming) { - transactionBalance += tx.value.amount!; - } else { - transactionBalance += -tx.value.amount! - tx.value.fee!; - } - } - - // TODO: grab total balance - var bal = 0; - for (var element in walletBase!.balance!.entries) { - bal = bal + element.value.fullBalance; - } - //todo: check if print needed - // debugPrint("balances: $transactionBalance $bal"); - if (isActive) { - String am = wowneroAmountToString(amount: bal); - - return Decimal.parse(am); - } else { - String am = wowneroAmountToString(amount: transactionBalance); - - return Decimal.parse(am); - } - } - - @override - // TODO: implement onIsActiveWalletChanged - void Function(bool)? get onIsActiveWalletChanged => (isActive) async { - await walletBase?.save(); - walletBase?.close(); - wowneroAutosaveTimer?.cancel(); - wowneroAutosaveTimer = null; - timer?.cancel(); - timer = null; - await stopSyncPercentTimer(); - if (isActive) { - String? password; - try { - password = - await keysStorage?.getWalletPassword(walletName: _walletId); - } catch (e, s) { - //todo: check if print needed - // debugPrint("Exception was thrown $e $s"); - throw Exception("Password not fou" - "*nd $e, $s"); - } - walletBase = (await walletService?.openWallet(_walletId, password!)) - as WowneroWalletBase?; - if (!(await walletBase!.isConnected())) { - final node = await getCurrentNode(); - final host = Uri.parse(node.host).host; - await walletBase?.connectToNode( - node: - Node(uri: "$host:${node.port}", type: WalletType.wownero)); - await walletBase?.startSync(); - } - await refresh(); - } - this.isActive = isActive; - }; - - bool isActive = false; - - @override - Future get transactionData => - _transactionData ??= _fetchTransactionData(); + Future? _currentReceivingAddress; + Future? _feeObject; Future? _transactionData; - // not used in wownero - TransactionData? cachedTxData; + Mutex prepareSendMutex = Mutex(); + Mutex estimateFeeMutex = Mutex(); - @override - Future updateSentCachedTxData(Map txData) async { - // not used in wownero - } - - Future _fetchTransactionData() async { - final transactions = walletBase?.transactionHistory!.transactions; - - final cachedTransactions = - DB.instance.get(boxName: walletId, key: 'latest_tx_model') - as TransactionData?; - int latestTxnBlockHeight = - DB.instance.get(boxName: walletId, key: "storedTxnDataHeight") - as int? ?? - 0; - - final txidsList = DB.instance - .get(boxName: walletId, key: "cachedTxids") as List? ?? - []; - - final Set cachedTxids = Set.from(txidsList); - - // TODO: filter to skip cached + confirmed txn processing in next step - // final unconfirmedCachedTransactions = - // cachedTransactions?.getAllTransactions() ?? {}; - // unconfirmedCachedTransactions - // .removeWhere((key, value) => value.confirmedStatus); - // - // if (cachedTransactions != null) { - // for (final tx in allTxHashes.toList(growable: false)) { - // final txHeight = tx["height"] as int; - // if (txHeight > 0 && - // txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) { - // if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) { - // allTxHashes.remove(tx); - // } - // } - // } - // } - - // sort thing stuff - // change to get Wownero price - final priceData = - await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); - Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; - final List> midSortedArray = []; - - if (transactions != null) { - for (var tx in transactions.entries) { - cachedTxids.add(tx.value.id); - Logging.instance.log( - "${tx.value.accountIndex} ${tx.value.addressIndex} ${tx.value.amount} ${tx.value.date} " - "${tx.value.direction} ${tx.value.fee} ${tx.value.height} ${tx.value.id} ${tx.value.isPending} ${tx.value.key} " - "${tx.value.recipientAddress}, ${tx.value.additionalInfo} con:${tx.value.confirmations}" - " ${tx.value.keyIndex}", - level: LogLevel.Info); - String am = wowneroAmountToString(amount: tx.value.amount!); - final worthNow = (currentPrice * Decimal.parse(am)).toStringAsFixed(2); - Map midSortedTx = {}; - // // create final tx map - midSortedTx["txid"] = tx.value.id; - midSortedTx["confirmed_status"] = !tx.value.isPending && - tx.value.confirmations != null && - tx.value.confirmations! >= MINIMUM_CONFIRMATIONS; - midSortedTx["confirmations"] = tx.value.confirmations ?? 0; - midSortedTx["timestamp"] = - (tx.value.date.millisecondsSinceEpoch ~/ 1000); - midSortedTx["txType"] = - tx.value.direction == TransactionDirection.incoming - ? "Received" - : "Sent"; - midSortedTx["amount"] = tx.value.amount; - midSortedTx["worthNow"] = worthNow; - midSortedTx["worthAtBlockTimestamp"] = worthNow; - midSortedTx["fees"] = tx.value.fee; - // TODO: shouldn't wownero have an address I can grab - if (tx.value.direction == TransactionDirection.incoming) { - final addressInfo = tx.value.additionalInfo; - - midSortedTx["address"] = walletBase?.getTransactionAddress( - addressInfo!['accountIndex'] as int, - addressInfo['addressIndex'] as int, - ); - } else { - midSortedTx["address"] = ""; - } - - final int txHeight = tx.value.height ?? 0; - midSortedTx["height"] = txHeight; - if (txHeight >= latestTxnBlockHeight) { - latestTxnBlockHeight = txHeight; - } - - midSortedTx["aliens"] = []; - midSortedTx["inputSize"] = 0; - midSortedTx["outputSize"] = 0; - midSortedTx["inputs"] = []; - midSortedTx["outputs"] = []; - midSortedArray.add(midSortedTx); - } - } - - // sort by date ---- - midSortedArray - .sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int)); - Logging.instance.log(midSortedArray, level: LogLevel.Info); - - // buildDateTimeChunks - final Map result = {"dateTimeChunks": []}; - final dateArray = []; - - for (int i = 0; i < midSortedArray.length; i++) { - final txObject = midSortedArray[i]; - final date = extractDateFromTimestamp(txObject["timestamp"] as int); - final txTimeArray = [txObject["timestamp"], date]; - - if (dateArray.contains(txTimeArray[1])) { - result["dateTimeChunks"].forEach((dynamic chunk) { - if (extractDateFromTimestamp(chunk["timestamp"] as int) == - txTimeArray[1]) { - if (chunk["transactions"] == null) { - chunk["transactions"] = >[]; - } - chunk["transactions"].add(txObject); - } - }); - } else { - dateArray.add(txTimeArray[1]); - final chunk = { - "timestamp": txTimeArray[0], - "transactions": [txObject], - }; - result["dateTimeChunks"].add(chunk); - } - } - - final transactionsMap = cachedTransactions?.getAllTransactions() ?? {}; - transactionsMap - .addAll(TransactionData.fromJson(result).getAllTransactions()); - - final txModel = TransactionData.fromMap(transactionsMap); - - await DB.instance.put( - boxName: walletId, - key: 'storedTxnDataHeight', - value: latestTxnBlockHeight); - await DB.instance.put( - boxName: walletId, key: 'latest_tx_model', value: txModel); - await DB.instance.put( - boxName: walletId, - key: 'cachedTxids', - value: cachedTxids.toList(growable: false)); - - return txModel; - } - - @override - // TODO: implement unspentOutputs - Future> get unspentOutputs => throw UnimplementedError(); - - @override - bool validateAddress(String address) { - bool valid = walletBase!.validateAddress(address); - return valid; - } - - @override - String get walletId => _walletId; - late String _walletId; - - @override - String get walletName => _walletName; - late String _walletName; - - // setter for updating on rename - @override - set walletName(String newName) => _walletName = newName; - - @override - set isFavorite(bool markFavorite) { - DB.instance.put( - boxName: walletId, key: "isFavorite", value: markFavorite); - } + WowneroWallet({ + required String walletId, + required String walletName, + required Coin coin, + required SecureStorageInterface secureStorage, + PriceAPI? priceAPI, + Prefs? prefs, + }) : _walletId = walletId, + _walletName = walletName, + _coin = coin, + _priceAPI = priceAPI ?? PriceAPI(Client()), + _secureStorage = secureStorage, + _prefs = prefs ?? Prefs.instance; @override bool get isFavorite { @@ -1408,17 +106,59 @@ class WowneroWallet extends CoinServiceAPI { } @override - // TODO: implement availableBalance - Future get availableBalance async { - var bal = 0; - for (var element in walletBase!.balance!.entries) { - bal = bal + element.value.unlockedBalance; - } - String am = wowneroAmountToString(amount: bal); - - return Decimal.parse(am); + set isFavorite(bool markFavorite) { + DB.instance.put( + boxName: walletId, key: "isFavorite", value: markFavorite); } + @override + bool get shouldAutoSync => _shouldAutoSync; + + @override + set shouldAutoSync(bool shouldAutoSync) { + if (_shouldAutoSync != shouldAutoSync) { + _shouldAutoSync = shouldAutoSync; + // wow wallets cannot be open at the same time + // leave following commented out for now + + // if (!shouldAutoSync) { + // timer?.cancel(); + // moneroAutosaveTimer?.cancel(); + // timer = null; + // moneroAutosaveTimer = null; + // stopNetworkAlivePinging(); + // } else { + // startNetworkAlivePinging(); + // // Walletbase needs to be open for this to work + // refresh(); + // } + } + } + + @override + String get walletName => _walletName; + + // setter for updating on rename + @override + set walletName(String newName) => _walletName = newName; + + @override + // not really used for wownero + Future> get allOwnAddresses async => []; + + @override + Future get availableBalance async { + int runningBalance = 0; + for (final entry in walletBase!.balance!.entries) { + runningBalance += entry.value.unlockedBalance; + } + return Format.satoshisToAmount(runningBalance, coin: coin); + } + + @override + // not used + Future get balanceMinusMaxFee => throw UnimplementedError(); + @override Coin get coin => _coin; @@ -1446,97 +186,9 @@ class WowneroWallet extends CoinServiceAPI { } } - // TODO: fix the double free memory crash error. @override - Future> prepareSend( - {required String address, - required int satoshiAmount, - Map? args}) async { - int amount = satoshiAmount; - String toAddress = address; - try { - final feeRate = args?["feeRate"]; - if (feeRate is FeeRateType) { - MoneroTransactionPriority feePriority; - switch (feeRate) { - case FeeRateType.fast: - feePriority = MoneroTransactionPriority.fast; - break; - case FeeRateType.average: - feePriority = MoneroTransactionPriority.regular; - break; - case FeeRateType.slow: - feePriority = MoneroTransactionPriority.slow; - break; - } - - Future? awaitPendingTransaction; - try { - // check for send all - bool isSendAll = false; - final balance = await availableBalance; - final satInDecimal = ((Decimal.fromInt(satoshiAmount) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal()); - if (satInDecimal == balance) { - isSendAll = true; - } - Logging.instance - .log("$toAddress $amount $args", level: LogLevel.Info); - String amountToSend = wowneroAmountToString(amount: amount); - Logging.instance.log("$amount $amountToSend", level: LogLevel.Info); - - wownero_output.Output output = wownero_output.Output(walletBase!); - output.address = toAddress; - output.sendAll = isSendAll; - output.setCryptoAmount(amountToSend); - - List outputs = [output]; - Object tmp = wownero.createWowneroTransactionCreationCredentials( - outputs: outputs, priority: feePriority); - - await prepareSendMutex.protect(() async { - awaitPendingTransaction = walletBase!.createTransaction(tmp); - }); - } catch (e, s) { - Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", - level: LogLevel.Warning); - } - - PendingWowneroTransaction pendingWowneroTransaction = - await (awaitPendingTransaction!) as PendingWowneroTransaction; - int realfee = Format.decimalAmountToSatoshis( - Decimal.parse(pendingWowneroTransaction.feeFormatted), coin); - //todo: check if print needed - // debugPrint("fee? $realfee"); - Map txData = { - "pendingWowneroTransaction": pendingWowneroTransaction, - "fee": realfee, - "addresss": toAddress, - "recipientAmt": satoshiAmount, - }; - - Logging.instance.log("prepare send: $txData", level: LogLevel.Info); - return txData; - } else { - throw ArgumentError("Invalid fee rate argument provided!"); - } - } catch (e, s) { - Logging.instance.log("Exception rethrown from prepare send(): $e\n$s", - level: LogLevel.Info); - - if (e.toString().contains("Incorrect unlocked balance")) { - throw Exception("Insufficient balance!"); - } else if (e is CreationTransactionException) { - throw Exception("Insufficient funds to pay for transaction fee!"); - } else { - throw Exception("Transaction failed with error code $e"); - } - } - } - - Mutex prepareSendMutex = Mutex(); - Mutex estimateFeeMutex = Mutex(); + Future get currentReceivingAddress => + _currentReceivingAddress ??= _getCurrentAddressForChain(0); @override Future estimateFeeFor(int satoshiAmount, int feeRate) async { @@ -1587,6 +239,32 @@ class WowneroWallet extends CoinServiceAPI { return fee; } + @override + Future exit() async { + if (!_hasCalledExit) { + walletBase?.onNewBlock = null; + walletBase?.onNewTransaction = null; + walletBase?.syncStatusChanged = null; + _hasCalledExit = true; + _autoSaveTimer?.cancel(); + await walletBase?.save(prioritySave: true); + walletBase?.close(); + } + } + + @override + Future get fees => _feeObject ??= _getFees(); + + @override + Future fullRescan( + int maxUnusedAddressGap, + int maxNumberOfIndexesToCheck, + ) async { + var restoreHeight = walletBase?.walletInfo.restoreHeight; + highestPercentCached = 0; + await walletBase?.rescan(height: restoreHeight); + } + @override Future generateNewAddress() async { try { @@ -1615,4 +293,1046 @@ class WowneroWallet extends CoinServiceAPI { return false; } } + + @override + bool get hasCalledExit => _hasCalledExit; + + @override + Future initializeExisting() async { + Logging.instance.log( + "Opening existing ${coin.prettyName} wallet $walletName...", + level: LogLevel.Info); + + if ((DB.instance.get(boxName: walletId, key: "id")) == null) { + //todo: check if print needed + // debugPrint("Exception was thrown"); + throw Exception( + "Attempted to initialize an existing wallet using an unknown wallet ID!"); + } + + walletService = + wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); + keysStorage = KeyService(_secureStorage); + + await _prefs.init(); + // final data = + // DB.instance.get(boxName: walletId, key: "latest_tx_model") + // as TransactionData?; + // if (data != null) { + // _transactionData = Future(() => data); + // } + + String? password; + try { + password = await keysStorage?.getWalletPassword(walletName: _walletId); + } catch (e, s) { + throw Exception("Password not found $e, $s"); + } + walletBase = (await walletService?.openWallet(_walletId, password!)) + as WowneroWalletBase; + + Logging.instance.log( + "Opened existing ${coin.prettyName} wallet $walletName", + level: LogLevel.Info, + ); + // Wallet already exists, triggers for a returning user + + String indexKey = "receivingIndex"; + final curIndex = + await DB.instance.get(boxName: walletId, key: indexKey) as int; + // Use new index to derive a new receiving address + final newReceivingAddress = await _generateAddressForChain(0, curIndex); + Logging.instance.log( + "wownero address in init existing: $newReceivingAddress", + level: LogLevel.Info); + _currentReceivingAddress = Future(() => newReceivingAddress); + } + + @override + Future initializeNew({int seedWordsLength = 14}) async { + await _prefs.init(); + + // this should never fail + if ((await _secureStorage.read(key: '${_walletId}_mnemonic')) != null) { + throw Exception( + "Attempted to overwrite mnemonic on generate new wallet!"); + } + + // TODO: Wallet Service may need to be switched to Wownero + walletService = + wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); + keysStorage = KeyService(_secureStorage); + WalletInfo walletInfo; + WalletCredentials credentials; + try { + String name = _walletId; + final dirPath = + await _pathForWalletDir(name: name, type: WalletType.wownero); + final path = await _pathForWallet(name: name, type: WalletType.wownero); + credentials = wownero.createWowneroNewWalletCredentials( + name: name, + language: "English", + seedWordsLength: seedWordsLength, + ); + + walletInfo = WalletInfo.external( + id: WalletBase.idFor(name, WalletType.wownero), + name: name, + type: WalletType.wownero, + isRecovery: false, + restoreHeight: credentials.height ?? 0, + date: DateTime.now(), + path: path, + dirPath: dirPath, + // TODO: find out what to put for address + address: '', + ); + credentials.walletInfo = walletInfo; + + _walletCreationService = WalletCreationService( + secureStorage: _secureStorage, + walletService: walletService, + keyService: keysStorage, + ); + _walletCreationService?.changeWalletType(); + // To restore from a seed + final wallet = await _walletCreationService?.create(credentials); + + final bufferedCreateHeight = (seedWordsLength == 14) + ? getSeedHeightSync(wallet?.seed.trim() as String) + : wownero.getHeightByDate( + date: DateTime.now().subtract(const Duration( + days: + 2))); // subtract a couple days to ensure we have a buffer for SWB + + await DB.instance.put( + boxName: walletId, key: "restoreHeight", value: bufferedCreateHeight); + walletInfo.restoreHeight = bufferedCreateHeight; + + await _secureStorage.write( + key: '${_walletId}_mnemonic', value: wallet?.seed.trim()); + + walletInfo.address = wallet?.walletAddresses.address; + await DB.instance + .add(boxName: WalletInfo.boxName, value: walletInfo); + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + } catch (e, s) { + debugPrint(e.toString()); + debugPrint(s.toString()); + walletBase?.close(); + } + final node = await _getCurrentNode(); + final host = Uri.parse(node.host).host; + await walletBase?.connectToNode( + node: Node(uri: "$host:${node.port}", type: WalletType.wownero)); + await walletBase?.startSync(); + await DB.instance + .put(boxName: walletId, key: "id", value: _walletId); + + // Set relevant indexes + await DB.instance + .put(boxName: walletId, key: "receivingIndex", value: 0); + await DB.instance + .put(boxName: walletId, key: "changeIndex", value: 0); + await DB.instance.put( + boxName: walletId, + key: 'blocked_tx_hashes', + value: ["0xdefault"], + ); // A list of transaction hashes to represent frozen utxos in wallet + // initialize address book entries + await DB.instance.put( + boxName: walletId, + key: 'addressBookEntries', + value: {}); + await DB.instance + .put(boxName: walletId, key: "isFavorite", value: false); + + // Generate and add addresses to relevant arrays + final initialReceivingAddress = await _generateAddressForChain(0, 0); + // final initialChangeAddress = await _generateAddressForChain(1, 0); + + await _addToAddressesArrayForChain(initialReceivingAddress, 0); + // await _addToAddressesArrayForChain(initialChangeAddress, 1); + + await DB.instance.put( + boxName: walletId, + key: 'receivingAddresses', + value: [initialReceivingAddress]); + await DB.instance + .put(boxName: walletId, key: "receivingIndex", value: 0); + walletBase?.close(); + _currentReceivingAddress = Future(() => initialReceivingAddress); + + Logging.instance + .log("initializeNew for $walletName $walletId", level: LogLevel.Info); + } + + @override + bool get isConnected => _isConnected; + + @override + bool get isRefreshing => refreshMutex; + + @override + // not used in wow + Future get maxFee => throw UnimplementedError(); + + @override + Future> get mnemonic async { + final mnemonicString = + await _secureStorage.read(key: '${_walletId}_mnemonic'); + if (mnemonicString == null) { + return []; + } + final List data = mnemonicString.split(' '); + return data; + } + + @override + // not used in wow + Future get pendingBalance => throw UnimplementedError(); + + @override + Future> prepareSend({ + required String address, + required int satoshiAmount, + Map? args, + }) async { + try { + final feeRate = args?["feeRate"]; + if (feeRate is FeeRateType) { + MoneroTransactionPriority feePriority; + switch (feeRate) { + case FeeRateType.fast: + feePriority = MoneroTransactionPriority.fast; + break; + case FeeRateType.average: + feePriority = MoneroTransactionPriority.regular; + break; + case FeeRateType.slow: + feePriority = MoneroTransactionPriority.slow; + break; + } + + Future? awaitPendingTransaction; + try { + // check for send all + bool isSendAll = false; + final balance = await availableBalance; + final satInDecimal = + Format.satoshisToAmount(satoshiAmount, coin: coin); + + if (satInDecimal == balance) { + isSendAll = true; + } + Logging.instance + .log("$address $satoshiAmount $args", level: LogLevel.Info); + String amountToSend = satInDecimal + .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + Logging.instance + .log("$satoshiAmount $amountToSend", level: LogLevel.Info); + + wownero_output.Output output = wownero_output.Output(walletBase!); + output.address = address; + output.sendAll = isSendAll; + output.setCryptoAmount(amountToSend); + + List outputs = [output]; + Object tmp = wownero.createWowneroTransactionCreationCredentials( + outputs: outputs, + priority: feePriority, + ); + + await prepareSendMutex.protect(() async { + awaitPendingTransaction = walletBase!.createTransaction(tmp); + }); + } catch (e, s) { + Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", + level: LogLevel.Warning); + } + + PendingWowneroTransaction pendingWowneroTransaction = + await (awaitPendingTransaction!) as PendingWowneroTransaction; + int realfee = Format.decimalAmountToSatoshis( + Decimal.parse(pendingWowneroTransaction.feeFormatted), coin); + //todo: check if print needed + // debugPrint("fee? $realfee"); + Map txData = { + "pendingWowneroTransaction": pendingWowneroTransaction, + "fee": realfee, + "addresss": address, + "recipientAmt": satoshiAmount, + }; + + Logging.instance.log("prepare send: $txData", level: LogLevel.Info); + return txData; + } else { + throw ArgumentError("Invalid fee rate argument provided!"); + } + } catch (e, s) { + Logging.instance.log("Exception rethrown from prepare send(): $e\n$s", + level: LogLevel.Info); + + if (e.toString().contains("Incorrect unlocked balance")) { + throw Exception("Insufficient balance!"); + } else if (e is CreationTransactionException) { + throw Exception("Insufficient funds to pay for transaction fee!"); + } else { + throw Exception("Transaction failed with error code $e"); + } + } + } + + @override + Future recoverFromMnemonic({ + required String mnemonic, + required int maxUnusedAddressGap, + required int maxNumberOfIndexesToCheck, + required int height, + }) async { + final int seedLength = mnemonic.trim().split(" ").length; + if (!(seedLength == 14 || seedLength == 25)) { + throw Exception("Invalid wownero mnemonic length found: $seedLength"); + } + + await _prefs.init(); + longMutex = true; + final start = DateTime.now(); + try { + // check to make sure we aren't overwriting a mnemonic + // this should never fail + if ((await _secureStorage.read(key: '${_walletId}_mnemonic')) != null) { + longMutex = false; + throw Exception("Attempted to overwrite mnemonic on restore!"); + } + await _secureStorage.write( + key: '${_walletId}_mnemonic', value: mnemonic.trim()); + + // extract seed height from 14 word seed + if (seedLength == 14) { + height = getSeedHeightSync(mnemonic.trim()); + } else { + // 25 word seed. TODO validate + if (height == 0) { + height = wownero.getHeightByDate( + date: DateTime.now().subtract(const Duration( + days: + 2))); // subtract a couple days to ensure we have a buffer for SWB\ + } + } + + await DB.instance + .put(boxName: walletId, key: "restoreHeight", value: height); + + walletService = + wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); + keysStorage = KeyService(_secureStorage); + WalletInfo walletInfo; + WalletCredentials credentials; + String name = _walletId; + final dirPath = + await _pathForWalletDir(name: name, type: WalletType.wownero); + final path = await _pathForWallet(name: name, type: WalletType.wownero); + credentials = wownero.createWowneroRestoreWalletFromSeedCredentials( + name: name, + height: height, + mnemonic: mnemonic.trim(), + ); + try { + walletInfo = WalletInfo.external( + id: WalletBase.idFor(name, WalletType.wownero), + name: name, + type: WalletType.wownero, + isRecovery: false, + restoreHeight: credentials.height ?? 0, + date: DateTime.now(), + path: path, + dirPath: dirPath, + // TODO: find out what to put for address + address: ''); + credentials.walletInfo = walletInfo; + + _walletCreationService = WalletCreationService( + secureStorage: _secureStorage, + walletService: walletService, + keyService: keysStorage, + ); + _walletCreationService!.changeWalletType(); + // To restore from a seed + final wallet = + await _walletCreationService!.restoreFromSeed(credentials); + walletInfo.address = wallet.walletAddresses.address; + await DB.instance + .add(boxName: WalletInfo.boxName, value: walletInfo); + walletBase?.close(); + walletBase = wallet as WowneroWalletBase; + await DB.instance.put( + boxName: walletId, + key: 'receivingAddresses', + value: [walletInfo.address!]); + await DB.instance + .put(boxName: walletId, key: "receivingIndex", value: 0); + await DB.instance + .put(boxName: walletId, key: "id", value: _walletId); + await DB.instance + .put(boxName: walletId, key: "changeIndex", value: 0); + await DB.instance.put( + boxName: walletId, + key: 'blocked_tx_hashes', + value: ["0xdefault"], + ); // A list of transaction hashes to represent frozen utxos in wallet + // initialize address book entries + await DB.instance.put( + boxName: walletId, + key: 'addressBookEntries', + value: {}); + await DB.instance + .put(boxName: walletId, key: "isFavorite", value: false); + } catch (e, s) { + //todo: come back to this + debugPrint(e.toString()); + debugPrint(s.toString()); + } + final node = await _getCurrentNode(); + final host = Uri.parse(node.host).host; + await walletBase?.connectToNode( + node: Node(uri: "$host:${node.port}", type: WalletType.wownero)); + await walletBase?.rescan(height: credentials.height); + walletBase?.close(); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from recoverFromMnemonic(): $e\n$s", + level: LogLevel.Error); + longMutex = false; + rethrow; + } + longMutex = false; + + final end = DateTime.now(); + Logging.instance.log( + "$walletName Recovery time: ${end.difference(start).inMilliseconds} millis", + level: LogLevel.Info); + } + + @override + Future refresh() async { + if (refreshMutex) { + Logging.instance.log("$walletId $walletName refreshMutex denied", + level: LogLevel.Info); + return; + } else { + refreshMutex = true; + } + + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + + final newTxData = await _fetchTransactionData(); + _transactionData = Future(() => newTxData); + + await _checkCurrentReceivingAddressesForTransactions(); + String indexKey = "receivingIndex"; + final curIndex = + DB.instance.get(boxName: walletId, key: indexKey) as int; + // Use new index to derive a new receiving address + try { + final newReceivingAddress = await _generateAddressForChain(0, curIndex); + _currentReceivingAddress = Future(() => newReceivingAddress); + } catch (e, s) { + Logging.instance.log( + "Failed to call _generateAddressForChain(0, $curIndex): $e\n$s", + level: LogLevel.Error); + } + + if (walletBase?.syncStatus is SyncedSyncStatus) { + refreshMutex = false; + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + coin, + ), + ); + } + } + + @override + Future send({ + required String toAddress, + required int amount, + Map args = const {}, + }) { + // not used for xmr + throw UnimplementedError(); + } + + @override + Future testNetworkConnection() async { + return await walletBase?.isConnected() ?? false; + } + + bool _isActive = false; + + @override + void Function(bool)? get onIsActiveWalletChanged => (isActive) async { + if (_isActive == isActive) { + return; + } + _isActive = isActive; + + if (isActive) { + _hasCalledExit = false; + String? password; + try { + password = + await keysStorage?.getWalletPassword(walletName: _walletId); + } catch (e, s) { + throw Exception("Password not found $e, $s"); + } + walletBase = (await walletService?.openWallet(_walletId, password!)) + as WowneroWalletBase?; + + walletBase!.onNewBlock = onNewBlock; + walletBase!.onNewTransaction = onNewTransaction; + walletBase!.syncStatusChanged = syncStatusChanged; + + if (!(await walletBase!.isConnected())) { + final node = await _getCurrentNode(); + final host = Uri.parse(node.host).host; + await walletBase?.connectToNode( + node: Node(uri: "$host:${node.port}", type: WalletType.monero)); + } + await walletBase?.startSync(); + await refresh(); + _autoSaveTimer?.cancel(); + _autoSaveTimer = Timer.periodic( + const Duration(seconds: 93), + (_) async => await walletBase?.save(), + ); + } else { + await exit(); + } + }; + + Future _updateCachedBalance(int sats) async { + await DB.instance.put( + boxName: walletId, + key: "cachedWowneroBalanceSats", + value: sats, + ); + } + + int _getCachedBalance() => + DB.instance.get( + boxName: walletId, + key: "cachedWowneroBalanceSats", + ) as int? ?? + 0; + + @override + Future get totalBalance async { + try { + final balanceEntries = walletBase?.balance?.entries; + if (balanceEntries != null) { + int bal = 0; + for (var element in balanceEntries) { + bal = bal + element.value.fullBalance; + } + await _updateCachedBalance(bal); + return Format.satoshisToAmount(bal, coin: coin); + } else { + final transactions = walletBase!.transactionHistory!.transactions; + int transactionBalance = 0; + for (var tx in transactions!.entries) { + if (tx.value.direction == TransactionDirection.incoming) { + transactionBalance += tx.value.amount!; + } else { + transactionBalance += -tx.value.amount! - tx.value.fee!; + } + } + + await _updateCachedBalance(transactionBalance); + return Format.satoshisToAmount(transactionBalance, coin: coin); + } + } catch (_) { + return Format.satoshisToAmount(_getCachedBalance(), coin: coin); + } + } + + @override + Future get transactionData => + _transactionData ??= _fetchTransactionData(); + + @override + // not used for xmr + Future> get unspentOutputs => throw UnimplementedError(); + + @override + Future updateNode(bool shouldRefresh) async { + final node = await _getCurrentNode(); + + final host = Uri.parse(node.host).host; + await walletBase?.connectToNode( + node: Node(uri: "$host:${node.port}", type: WalletType.monero)); + + // TODO: is this sync call needed? Do we need to notify ui here? + await walletBase?.startSync(); + + if (shouldRefresh) { + await refresh(); + } + } + + @override + Future updateSentCachedTxData(Map txData) async { + // not used for xmr + return; + } + + @override + bool validateAddress(String address) => walletBase!.validateAddress(address); + + @override + String get walletId => _walletId; + + /// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain] + /// and + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future _getCurrentAddressForChain(int chain) async { + // Here, we assume that chain == 1 if it isn't 0 + String arrayKey = chain == 0 ? "receivingAddresses" : "changeAddresses"; + final internalChainArray = (DB.instance + .get(boxName: walletId, key: arrayKey)) as List; + return internalChainArray.last as String; + } + + /// Increases the index for either the internal or external chain, depending on [chain]. + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future _incrementAddressIndexForChain(int chain) async { + // Here we assume chain == 1 if it isn't 0 + String indexKey = chain == 0 ? "receivingIndex" : "changeIndex"; + + final newIndex = + (DB.instance.get(boxName: walletId, key: indexKey)) + 1; + await DB.instance + .put(boxName: walletId, key: indexKey, value: newIndex); + } + + Future _generateAddressForChain(int chain, int index) async { + // + String address = walletBase!.getTransactionAddress(chain, index); + + return address; + } + + /// Adds [address] to the relevant chain's address array, which is determined by [chain]. + /// [address] - Expects a standard native segwit address + /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value! + Future _addToAddressesArrayForChain(String address, int chain) async { + String chainArray = ''; + if (chain == 0) { + chainArray = 'receivingAddresses'; + } else { + chainArray = 'changeAddresses'; + } + + final addressArray = + DB.instance.get(boxName: walletId, key: chainArray); + if (addressArray == null) { + Logging.instance.log( + 'Attempting to add the following to $chainArray array for chain $chain:${[ + address + ]}', + level: LogLevel.Info); + await DB.instance + .put(boxName: walletId, key: chainArray, value: [address]); + } else { + // Make a deep copy of the existing list + final List newArray = []; + addressArray + .forEach((dynamic _address) => newArray.add(_address as String)); + newArray.add(address); // Add the address passed into the method + await DB.instance + .put(boxName: walletId, key: chainArray, value: newArray); + } + } + + Future _getFees() async { + // TODO: not use random hard coded values here + return FeeObject( + numberOfBlocksFast: 10, + numberOfBlocksAverage: 15, + numberOfBlocksSlow: 20, + fast: MoneroTransactionPriority.fast.raw!, + medium: MoneroTransactionPriority.regular.raw!, + slow: MoneroTransactionPriority.slow.raw!, + ); + } + + Future _fetchTransactionData() async { + await walletBase!.updateTransactions(); + final transactions = walletBase?.transactionHistory!.transactions; + + // final cachedTransactions = + // DB.instance.get(boxName: walletId, key: 'latest_tx_model') + // as TransactionData?; + // int latestTxnBlockHeight = + // DB.instance.get(boxName: walletId, key: "storedTxnDataHeight") + // as int? ?? + // 0; + // + // final txidsList = DB.instance + // .get(boxName: walletId, key: "cachedTxids") as List? ?? + // []; + // + // final Set cachedTxids = Set.from(txidsList); + + // TODO: filter to skip cached + confirmed txn processing in next step + // final unconfirmedCachedTransactions = + // cachedTransactions?.getAllTransactions() ?? {}; + // unconfirmedCachedTransactions + // .removeWhere((key, value) => value.confirmedStatus); + // + // if (cachedTransactions != null) { + // for (final tx in allTxHashes.toList(growable: false)) { + // final txHeight = tx["height"] as int; + // if (txHeight > 0 && + // txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) { + // if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) { + // allTxHashes.remove(tx); + // } + // } + // } + // } + + // sort thing stuff + // change to get Wownero price + final priceData = + await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; + final List> midSortedArray = []; + + if (transactions != null) { + for (var tx in transactions.entries) { + // cachedTxids.add(tx.value.id); + Logging.instance.log( + "${tx.value.accountIndex} ${tx.value.addressIndex} ${tx.value.amount} ${tx.value.date} " + "${tx.value.direction} ${tx.value.fee} ${tx.value.height} ${tx.value.id} ${tx.value.isPending} ${tx.value.key} " + "${tx.value.recipientAddress}, ${tx.value.additionalInfo} con:${tx.value.confirmations}" + " ${tx.value.keyIndex}", + level: LogLevel.Info); + String am = wowneroAmountToString(amount: tx.value.amount!); + final worthNow = (currentPrice * Decimal.parse(am)).toStringAsFixed(2); + Map midSortedTx = {}; + // // create final tx map + midSortedTx["txid"] = tx.value.id; + midSortedTx["confirmed_status"] = !tx.value.isPending && + tx.value.confirmations != null && + tx.value.confirmations! >= MINIMUM_CONFIRMATIONS; + midSortedTx["confirmations"] = tx.value.confirmations ?? 0; + midSortedTx["timestamp"] = + (tx.value.date.millisecondsSinceEpoch ~/ 1000); + midSortedTx["txType"] = + tx.value.direction == TransactionDirection.incoming + ? "Received" + : "Sent"; + midSortedTx["amount"] = tx.value.amount; + midSortedTx["worthNow"] = worthNow; + midSortedTx["worthAtBlockTimestamp"] = worthNow; + midSortedTx["fees"] = tx.value.fee; + // TODO: shouldn't wownero have an address I can grab + if (tx.value.direction == TransactionDirection.incoming) { + final addressInfo = tx.value.additionalInfo; + + midSortedTx["address"] = walletBase?.getTransactionAddress( + addressInfo!['accountIndex'] as int, + addressInfo['addressIndex'] as int, + ); + } else { + midSortedTx["address"] = ""; + } + + final int txHeight = tx.value.height ?? 0; + midSortedTx["height"] = txHeight; + // if (txHeight >= latestTxnBlockHeight) { + // latestTxnBlockHeight = txHeight; + // } + + midSortedTx["aliens"] = []; + midSortedTx["inputSize"] = 0; + midSortedTx["outputSize"] = 0; + midSortedTx["inputs"] = []; + midSortedTx["outputs"] = []; + midSortedArray.add(midSortedTx); + } + } + + // sort by date ---- + midSortedArray + .sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int)); + Logging.instance.log(midSortedArray, level: LogLevel.Info); + + // buildDateTimeChunks + final Map result = {"dateTimeChunks": []}; + final dateArray = []; + + for (int i = 0; i < midSortedArray.length; i++) { + final txObject = midSortedArray[i]; + final date = extractDateFromTimestamp(txObject["timestamp"] as int); + final txTimeArray = [txObject["timestamp"], date]; + + if (dateArray.contains(txTimeArray[1])) { + result["dateTimeChunks"].forEach((dynamic chunk) { + if (extractDateFromTimestamp(chunk["timestamp"] as int) == + txTimeArray[1]) { + if (chunk["transactions"] == null) { + chunk["transactions"] = >[]; + } + chunk["transactions"].add(txObject); + } + }); + } else { + dateArray.add(txTimeArray[1]); + final chunk = { + "timestamp": txTimeArray[0], + "transactions": [txObject], + }; + result["dateTimeChunks"].add(chunk); + } + } + + // final transactionsMap = cachedTransactions?.getAllTransactions() ?? {}; + final Map transactionsMap = {}; + transactionsMap + .addAll(TransactionData.fromJson(result).getAllTransactions()); + + final txModel = TransactionData.fromMap(transactionsMap); + // + // await DB.instance.put( + // boxName: walletId, + // key: 'storedTxnDataHeight', + // value: latestTxnBlockHeight); + // await DB.instance.put( + // boxName: walletId, key: 'latest_tx_model', value: txModel); + // await DB.instance.put( + // boxName: walletId, + // key: 'cachedTxids', + // value: cachedTxids.toList(growable: false)); + + return txModel; + } + + Future _pathForWalletDir({ + required String name, + required WalletType type, + }) async { + Directory root = await StackFileSystem.applicationRootDirectory(); + + final prefix = walletTypeToString(type).toLowerCase(); + final walletsDir = Directory('${root.path}/wallets'); + final walletDire = Directory('${walletsDir.path}/$prefix/$name'); + + if (!walletDire.existsSync()) { + walletDire.createSync(recursive: true); + } + + return walletDire.path; + } + + Future _pathForWallet({ + required String name, + required WalletType type, + }) async => + await _pathForWalletDir(name: name, type: type) + .then((path) => '$path/$name'); + + Future _getCurrentNode() async { + return NodeService(secureStorageInterface: _secureStorage) + .getPrimaryNodeFor(coin: coin) ?? + DefaultNodes.getNodeFor(coin); + } + + void onNewBlock() { + // + print("============================="); + print("New Wownero Block! :: $walletName"); + print("============================="); + } + + void onNewTransaction() { + // + print("============================="); + print("New Wownero Transaction! :: $walletName"); + print("============================="); + + // call this here? + GlobalEventBus.instance.fire( + UpdatedInBackgroundEvent( + "New data found in $walletId $walletName in background!", + walletId, + ), + ); + } + + void syncStatusChanged() async { + final syncStatus = walletBase?.syncStatus; + if (syncStatus != null) { + if (syncStatus.progress() == 1) { + refreshMutex = false; + } + + WalletSyncStatus? status; + _isConnected = true; + + if (syncStatus is SyncingSyncStatus) { + final int blocksLeft = syncStatus.blocksLeft; + + // ensure at least 1 to prevent math errors + final int height = max(1, syncStatus.height); + + final nodeHeight = height + blocksLeft; + + final percent = height / nodeHeight; + + final highest = max(highestPercentCached, percent); + + // update cached + if (highestPercentCached < percent) { + highestPercentCached = percent; + } + + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent( + highest, + walletId, + ), + ); + GlobalEventBus.instance.fire( + BlocksRemainingEvent( + blocksLeft, + walletId, + ), + ); + } else if (syncStatus is SyncedSyncStatus) { + status = WalletSyncStatus.synced; + } else if (syncStatus is NotConnectedSyncStatus) { + status = WalletSyncStatus.unableToSync; + _isConnected = false; + } else if (syncStatus is StartingSyncStatus) { + status = WalletSyncStatus.syncing; + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent( + highestPercentCached, + walletId, + ), + ); + } else if (syncStatus is FailedSyncStatus) { + status = WalletSyncStatus.unableToSync; + _isConnected = false; + } else if (syncStatus is ConnectingSyncStatus) { + status = WalletSyncStatus.syncing; + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent( + highestPercentCached, + walletId, + ), + ); + } else if (syncStatus is ConnectedSyncStatus) { + status = WalletSyncStatus.syncing; + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent( + highestPercentCached, + walletId, + ), + ); + } else if (syncStatus is LostConnectionSyncStatus) { + status = WalletSyncStatus.unableToSync; + _isConnected = false; + } + + if (status != null) { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + status, + walletId, + coin, + ), + ); + } + } + } + + Future _checkCurrentReceivingAddressesForTransactions() async { + try { + await _checkReceivingAddressForTransactions(); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future _checkReceivingAddressForTransactions() async { + try { + int highestIndex = -1; + for (var element + in walletBase!.transactionHistory!.transactions!.entries) { + if (element.value.direction == TransactionDirection.incoming) { + int curAddressIndex = + element.value.additionalInfo!['addressIndex'] as int; + if (curAddressIndex > highestIndex) { + highestIndex = curAddressIndex; + } + } + } + + // Check the new receiving index + String indexKey = "receivingIndex"; + final curIndex = + DB.instance.get(boxName: walletId, key: indexKey) as int; + if (highestIndex >= curIndex) { + // First increment the receiving index + await _incrementAddressIndexForChain(0); + final newReceivingIndex = + DB.instance.get(boxName: walletId, key: indexKey) as int; + + // Use new index to derive a new receiving address + final newReceivingAddress = + await _generateAddressForChain(0, newReceivingIndex); + + // Add that new receiving address to the array of receiving addresses + await _addToAddressesArrayForChain(newReceivingAddress, 0); + + _currentReceivingAddress = Future(() => newReceivingAddress); + } + } on SocketException catch (se, s) { + Logging.instance.log( + "SocketException caught in _checkReceivingAddressForTransactions(): $se\n$s", + level: LogLevel.Error); + return; + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkReceivingAddressForTransactions(): $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + double get highestPercentCached => + DB.instance.get(boxName: walletId, key: "highestPercentCached") + as double? ?? + 0; + + set highestPercentCached(double value) => DB.instance.put( + boxName: walletId, + key: "highestPercentCached", + value: value, + ); } diff --git a/lib/services/locale_service.dart b/lib/services/locale_service.dart index 9ed504cf9..e91f5a1c6 100644 --- a/lib/services/locale_service.dart +++ b/lib/services/locale_service.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'package:devicelocale/devicelocale.dart'; import 'package:flutter/material.dart'; @@ -7,7 +8,7 @@ class LocaleService extends ChangeNotifier { String get locale => _locale; Future loadLocale({bool notify = true}) async { - _locale = await Devicelocale.currentLocale ?? "en_US"; + _locale =Platform.isWindows ? "en_US" : await Devicelocale.currentLocale ?? "en_US"; if (notify) { notifyListeners(); } diff --git a/lib/services/notes_service.dart b/lib/services/notes_service.dart index cffe770e5..013600625 100644 --- a/lib/services/notes_service.dart +++ b/lib/services/notes_service.dart @@ -51,6 +51,7 @@ class NotesService extends ChangeNotifier { _notes[txid] = note; await DB.instance .put(boxName: walletId, key: 'notes', value: _notes); + //todo: check if this is needed Logging.instance.log("editOrAddNote: tx note saved", level: LogLevel.Info); await _refreshNotes(); } diff --git a/lib/services/wallets.dart b/lib/services/wallets.dart index faed86f75..9d1b97cee 100644 --- a/lib/services/wallets.dart +++ b/lib/services/wallets.dart @@ -238,7 +238,7 @@ class Wallets extends ChangeNotifier { walletIdsToEnableAutoSync.contains(manager.walletId); if (manager.coin == Coin.monero || manager.coin == Coin.wownero) { - walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); + // walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); } else { walletInitFutures.add(manager.initializeExisting().then((value) { if (shouldSetAutoSync) { @@ -328,7 +328,7 @@ class Wallets extends ChangeNotifier { walletIdsToEnableAutoSync.contains(manager.walletId); if (manager.coin == Coin.monero || manager.coin == Coin.wownero) { - walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); + // walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); } else { walletInitFutures.add(manager.initializeExisting().then((value) { if (shouldSetAutoSync) { diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 1d1f6e15d..a517992de 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -14,7 +14,6 @@ import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart' import 'package:stackwallet/services/coins/particl/particl_wallet.dart' as particl; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart' as wow; -import 'package:stackwallet/utilities/util.dart'; enum Coin { bitcoin, @@ -40,7 +39,7 @@ enum Coin { firoTestNet, } -final int kTestNetCoinCount = Util.isDesktop ? 5 : 4; +final int kTestNetCoinCount = 4; // Util.isDesktop ? 5 : 4; extension CoinExt on Coin { String get prettyName { diff --git a/lib/utilities/stack_file_system.dart b/lib/utilities/stack_file_system.dart index 5177f1973..22078bfee 100644 --- a/lib/utilities/stack_file_system.dart +++ b/lib/utilities/stack_file_system.dart @@ -15,8 +15,7 @@ abstract class StackFileSystem { } else if (Platform.isLinux) { appDirectory = Directory("${Platform.environment['HOME']}/.stackwallet"); } else if (Platform.isWindows) { - // TODO: windows root .stackwallet dir location - throw Exception("Unsupported platform"); + appDirectory = await getApplicationSupportDirectory(); } else if (Platform.isMacOS) { // currently run in ipad mode?? throw Exception("Unsupported platform"); diff --git a/lib/widgets/textfields/exchange_textfield.dart b/lib/widgets/textfields/exchange_textfield.dart index 399d077c4..6681e9e8e 100644 --- a/lib/widgets/textfields/exchange_textfield.dart +++ b/lib/widgets/textfields/exchange_textfield.dart @@ -4,6 +4,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; class ExchangeTextField extends StatefulWidget { @@ -62,6 +63,8 @@ class _ExchangeTextFieldState extends State { late final void Function(String)? onChanged; late final void Function(String)? onSubmitted; + final isDesktop = Util.isDesktop; + @override void initState() { borderRadius = widget.borderRadius; @@ -100,10 +103,12 @@ class _ExchangeTextFieldState extends State { enableSuggestions: false, autocorrect: false, readOnly: widget.readOnly, - keyboardType: const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), decoration: InputDecoration( contentPadding: const EdgeInsets.only( top: 12, diff --git a/lib/widgets/wallet_card.dart b/lib/widgets/wallet_card.dart index 0e8a515bd..194033de3 100644 --- a/lib/widgets/wallet_card.dart +++ b/lib/widgets/wallet_card.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart'; import 'package:tuple/tuple.dart'; @@ -31,7 +32,14 @@ class WalletSheetCard extends ConsumerWidget { Constants.size.circularBorderRadius, ), ), - onPressed: () { + onPressed: () async { + final manager = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId); + if (manager.coin == Coin.monero || + manager.coin == Coin.wownero) { + await manager.initializeExisting(); + } if (popPrevious) Navigator.of(context).pop(); Navigator.of(context).pushNamed( WalletView.routeName, diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 6cc16a184..6506a5075 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -55,7 +55,7 @@ /* Begin PBXFileReference section */ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* firo_wallet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = firo_wallet.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* Stack Wallet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Stack Wallet.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -112,7 +112,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* firo_wallet.app */, + 33CC10ED2044A3C60003C045 /* Stack Wallet.app */, ); name = Products; sourceTree = ""; @@ -193,7 +193,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* firo_wallet.app */; + productReference = 33CC10ED2044A3C60003C045 /* Stack Wallet.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 9c847111c..c9216c9be 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -31,7 +31,7 @@ @@ -54,7 +54,7 @@ @@ -71,7 +71,7 @@ diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index b68859075..6889aa0a7 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -5,10 +5,10 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = firo_wallet +PRODUCT_NAME = Stack Wallet // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.firoWallet +PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stack_wallet // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2022 com.cypherstack. All rights reserved. diff --git a/pubspec.yaml b/pubspec.yaml index b5e85fb26..2ad0f75c6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.25+98 +version: 1.5.27+100 environment: sdk: ">=2.17.0 <3.0.0" diff --git a/scripts/prebuild.sh b/scripts/prebuild.sh index b9ab666a9..77d65b253 100755 --- a/scripts/prebuild.sh +++ b/scripts/prebuild.sh @@ -1,3 +1,5 @@ +#!/bin/bash + # Create template lib/external_api_keys.dart file if it doesn't already exist KEYS=../lib/external_api_keys.dart if ! test -f "$KEYS"; then @@ -6,13 +8,14 @@ if ! test -f "$KEYS"; then fi # Create template wallet test parameter files if they don't already exist -declare -a coins=("bitcoin" "bitcoincash" "dogecoin" "namecoin" "firo" "particl") # TODO add monero and wownero when those tests are updated to use the .gitignored test wallet setup: when doing that, make sure to update the test vectors for a new, private development seed +declare -a coins +coins=("bitcoin" "bitcoincash" "dogecoin" "namecoin" "firo" "particl") # TODO add monero and wownero when those tests are updated to use the .gitignored test wallet setup: when doing that, make sure to update the test vectors for a new, private development seed for coin in "${coins[@]}" do WALLETTESTPARAMFILE="../test/services/coins/${coin}/${coin}_wallet_test_parameters.dart" if ! test -f "$WALLETTESTPARAMFILE"; then echo "prebuild.sh: creating template test/services/coins/${coin}/${coin}_wallet_test_parameters.dart file" - printf 'const TEST_MNEMONIC = "";\nconst ROOT_WIF = "";\nconst NODE_WIF_84 = "";\n' > $WALLETTESTPARAMFILE + printf 'const TEST_MNEMONIC = "";\nconst ROOT_WIF = "";\nconst NODE_WIF_84 = "";\n' > "$WALLETTESTPARAMFILE" fi done diff --git a/scripts/windows/build_all.sh b/scripts/windows/build_all.sh new file mode 100755 index 000000000..d890ed70a --- /dev/null +++ b/scripts/windows/build_all.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +mkdir -p build +(cd ../../crypto_plugins/flutter_libepiccash/scripts/windows && ./build_all.sh ) & +(cd ../../crypto_plugins/flutter_liblelantus/scripts/windows && ./build_all.sh ) & +(cd ../../crypto_plugins/flutter_libmonero/scripts/windows && ./build_all.sh) & + +wait +echo "Done building" diff --git a/test/widget_tests/wallet_card_test.dart b/test/widget_tests/wallet_card_test.dart index 834122da8..1dc82f9b1 100644 --- a/test/widget_tests/wallet_card_test.dart +++ b/test/widget_tests/wallet_card_test.dart @@ -43,6 +43,9 @@ void main() { mockito .when(wallets.getManagerProvider("wallet id")) .thenAnswer((realInvocation) => managerProvider); + mockito + .when(wallets.getManager("wallet id")) + .thenAnswer((realInvocation) => manager); final navigator = mockingjay.MockNavigator(); mockingjay diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index 871652d68..5fa3067e5 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -90,12 +90,12 @@ BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.cypherstack" "\0" - VALUE "FileDescription", "firo_wallet" "\0" + VALUE "FileDescription", "Stack Wallet" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "firo_wallet" "\0" + VALUE "InternalName", "Stack Wallet" "\0" VALUE "LegalCopyright", "Copyright (C) 2022 com.cypherstack. All rights reserved." "\0" - VALUE "OriginalFilename", "firo_wallet.exe" "\0" - VALUE "ProductName", "firo_wallet" "\0" + VALUE "OriginalFilename", "Stack Wallet.exe" "\0" + VALUE "ProductName", "Stack Wallet" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END