diff --git a/assets/icon/macos-icon.png b/assets/icon/macos-icon.png new file mode 100644 index 000000000..e4a263022 Binary files /dev/null and b/assets/icon/macos-icon.png differ diff --git a/crypto_plugins/flutter_libepiccash b/crypto_plugins/flutter_libepiccash index 686559344..f677dec0b 160000 --- a/crypto_plugins/flutter_libepiccash +++ b/crypto_plugins/flutter_libepiccash @@ -1 +1 @@ -Subproject commit 686559344a58f77732c3d0134fbf44d271a55229 +Subproject commit f677dec0b34d3f9fe8fce2bc8ff5c508c3f3bb9a diff --git a/crypto_plugins/flutter_liblelantus b/crypto_plugins/flutter_liblelantus index cdccef0e8..9cd241b5e 160000 --- a/crypto_plugins/flutter_liblelantus +++ b/crypto_plugins/flutter_liblelantus @@ -1 +1 @@ -Subproject commit cdccef0e8dc10b7fe703b5bb9b41b59b25177e83 +Subproject commit 9cd241b5ea142e21c01dd7639b42603281c43287 diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index c920c09df..e48952185 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit c920c09df5e415bba4bbe95dd50e1f0085f040e6 +Subproject commit e48952185556a10f182184fd572bcb04365f5831 diff --git a/lib/dto/ethereum/eth_token_tx_extra_dto.dart b/lib/dto/ethereum/eth_token_tx_extra_dto.dart index 144194914..aab41bb76 100644 --- a/lib/dto/ethereum/eth_token_tx_extra_dto.dart +++ b/lib/dto/ethereum/eth_token_tx_extra_dto.dart @@ -46,7 +46,7 @@ class EthTokenTxExtraDTO { ), gas: _amountFromJsonNum(map['gas']), gasPrice: _amountFromJsonNum(map['gasPrice']), - nonce: map['nonce'] as int, + nonce: map['nonce'] as int?, input: map['input'] as String, gasCost: _amountFromJsonNum(map['gasCost']), gasUsed: _amountFromJsonNum(map['gasUsed']), @@ -63,7 +63,7 @@ class EthTokenTxExtraDTO { final Amount gas; final Amount gasPrice; final String input; - final int nonce; + final int? nonce; final Amount gasCost; final Amount gasUsed; diff --git a/lib/dto/ethereum/eth_tx_dto.dart b/lib/dto/ethereum/eth_tx_dto.dart index 7289cda15..260675cf9 100644 --- a/lib/dto/ethereum/eth_tx_dto.dart +++ b/lib/dto/ethereum/eth_tx_dto.dart @@ -127,16 +127,16 @@ class EthTxDTO { map['timestamp'] = timestamp; map['from'] = from; map['to'] = to; - map['value'] = value; - map['gas'] = gas; - map['gasPrice'] = gasPrice; - map['maxFeePerGas'] = maxFeePerGas; - map['maxPriorityFeePerGas'] = maxPriorityFeePerGas; + map['value'] = value.toString(); + map['gas'] = gas.toString(); + map['gasPrice'] = gasPrice.toString(); + map['maxFeePerGas'] = maxFeePerGas.toString(); + map['maxPriorityFeePerGas'] = maxPriorityFeePerGas.toString(); map['isError'] = isError; map['hasToken'] = hasToken; map['compressedTx'] = compressedTx; - map['gasCost'] = gasCost; - map['gasUsed'] = gasUsed; + map['gasCost'] = gasCost.toString(); + map['gasUsed'] = gasUsed.toString(); return map; } diff --git a/lib/models/isar/stack_theme.dart b/lib/models/isar/stack_theme.dart index 557090584..32878366a 100644 --- a/lib/models/isar/stack_theme.dart +++ b/lib/models/isar/stack_theme.dart @@ -2341,8 +2341,6 @@ class ThemeAssetsV3 implements IThemeAssets { // Added some future proof params in case we want to add anything else // This should provide some buffer in stead of creating assetsV4 etc - @Name("otherStringParam1") - late final String? dummy1; @Name("otherStringParam2") late final String? dummy2; @Name("otherStringParam3") @@ -2388,6 +2386,19 @@ class ThemeAssetsV3 implements IThemeAssets { Map? _coinCardImages; late final String? coinCardImagesString; + @ignore + Map? get coinCardFavoritesImages => + _coinCardFavoritesImages ??= coinCardFavoritesImagesString == null + ? null + : parseCoinAssetsString( + coinCardFavoritesImagesString!, + placeHolder: coinPlaceholder, + ); + @ignore + Map? _coinCardFavoritesImages; + @Name("otherStringParam1") + late final String? coinCardFavoritesImagesString; + ThemeAssetsV3(); factory ThemeAssetsV3.fromJson({ @@ -2443,13 +2454,18 @@ class ThemeAssetsV3 implements IThemeAssets { Map.from(json["coins"]["cards"] as Map), ) : null + ..coinCardFavoritesImagesString = json["coins"]["favoriteCards"] is Map + ? createCoinAssetsString( + "$themeId/assets", + Map.from(json["coins"]["favoriteCards"] as Map), + ) + : null ..loadingGifRelative = json["loading_gif"] is String ? "$themeId/assets/${json["loading_gif"] as String}" : null ..backgroundRelative = json["background"] is String ? "$themeId/assets/${json["background"] as String}" : null - ..dummy1 = null ..dummy2 = null ..dummy3 = null; } @@ -2528,6 +2544,7 @@ class ThemeAssetsV3 implements IThemeAssets { 'coinImages: $coinImages, ' 'coinSecondaryImages: $coinSecondaryImages, ' 'coinCardImages: $coinCardImages' + 'coinCardFavoritesImages: $coinCardFavoritesImages' ')'; } } diff --git a/lib/models/isar/stack_theme.g.dart b/lib/models/isar/stack_theme.g.dart index bf38c461d..03c543ff0 100644 --- a/lib/models/isar/stack_theme.g.dart +++ b/lib/models/isar/stack_theme.g.dart @@ -29626,7 +29626,7 @@ int _themeAssetsV3EstimateSize( } } { - final value = object.dummy1; + final value = object.coinCardFavoritesImagesString; if (value != null) { bytesCount += 3 + value.length * 3; } @@ -29677,7 +29677,7 @@ void _themeAssetsV3Serialize( writer.writeString(offsets[7], object.coinSecondaryImagesString); writer.writeString(offsets[8], object.exchangeRelative); writer.writeString(offsets[9], object.loadingGifRelative); - writer.writeString(offsets[10], object.dummy1); + writer.writeString(offsets[10], object.coinCardFavoritesImagesString); writer.writeString(offsets[11], object.dummy2); writer.writeString(offsets[12], object.dummy3); writer.writeString(offsets[13], object.personaEasyRelative); @@ -29714,7 +29714,7 @@ ThemeAssetsV3 _themeAssetsV3Deserialize( object.coinSecondaryImagesString = reader.readString(offsets[7]); object.exchangeRelative = reader.readString(offsets[8]); object.loadingGifRelative = reader.readStringOrNull(offsets[9]); - object.dummy1 = reader.readStringOrNull(offsets[10]); + object.coinCardFavoritesImagesString = reader.readStringOrNull(offsets[10]); object.dummy2 = reader.readStringOrNull(offsets[11]); object.dummy3 = reader.readStringOrNull(offsets[12]); object.personaEasyRelative = reader.readString(offsets[13]); @@ -31224,7 +31224,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1IsNull() { + coinCardFavoritesImagesStringIsNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNull( property: r'otherStringParam1', @@ -31233,7 +31233,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1IsNotNull() { + coinCardFavoritesImagesStringIsNotNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNotNull( property: r'otherStringParam1', @@ -31242,7 +31242,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1EqualTo( + coinCardFavoritesImagesStringEqualTo( String? value, { bool caseSensitive = true, }) { @@ -31256,7 +31256,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1GreaterThan( + coinCardFavoritesImagesStringGreaterThan( String? value, { bool include = false, bool caseSensitive = true, @@ -31272,7 +31272,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1LessThan( + coinCardFavoritesImagesStringLessThan( String? value, { bool include = false, bool caseSensitive = true, @@ -31288,7 +31288,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1Between( + coinCardFavoritesImagesStringBetween( String? lower, String? upper, { bool includeLower = true, @@ -31308,7 +31308,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1StartsWith( + coinCardFavoritesImagesStringStartsWith( String value, { bool caseSensitive = true, }) { @@ -31322,7 +31322,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1EndsWith( + coinCardFavoritesImagesStringEndsWith( String value, { bool caseSensitive = true, }) { @@ -31336,7 +31336,8 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1Contains(String value, {bool caseSensitive = true}) { + coinCardFavoritesImagesStringContains(String value, + {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.contains( property: r'otherStringParam1', @@ -31347,7 +31348,8 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1Matches(String pattern, {bool caseSensitive = true}) { + coinCardFavoritesImagesStringMatches(String pattern, + {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.matches( property: r'otherStringParam1', @@ -31358,7 +31360,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1IsEmpty() { + coinCardFavoritesImagesStringIsEmpty() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( property: r'otherStringParam1', @@ -31368,7 +31370,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1IsNotEmpty() { + coinCardFavoritesImagesStringIsNotEmpty() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.greaterThan( property: r'otherStringParam1', diff --git a/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart index a3d297b8c..dd807976b 100644 --- a/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart +++ b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart @@ -142,7 +142,7 @@ class _AddCustomTokenViewState extends ConsumerState { context: context, message: "Looking up contract", ); - currentToken = response.value; + currentToken = response!.value; if (currentToken != null) { nameController.text = currentToken!.name; symbolController.text = currentToken!.symbol; @@ -157,7 +157,7 @@ class _AddCustomTokenViewState extends ConsumerState { context: context, builder: (context) => StackOkDialog( title: "Failed to look up token", - message: response.exception?.message, + message: response!.exception?.message, ), ), ); diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 3193e7bb7..a1fc66f21 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -493,51 +493,54 @@ class _ConfirmTransactionViewState ], ), ), - if (coin == Coin.epicCash) + if (coin == Coin.epicCash && + (transactionInfo["onChainNote"] as String).isNotEmpty) const SizedBox( height: 12, ), - if (coin == Coin.epicCash) + if (coin == Coin.epicCash && + (transactionInfo["onChainNote"] as String).isNotEmpty) RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "On chain note", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), - Text( - transactionInfo["onChainNote"] as String, - style: STextStyles.itemSubtitle12(context), - ), - ], + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "On chain note", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + transactionInfo["onChainNote"] as String, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - (coin == Coin.epicCash) ? "Local Note" : - "Note", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), - Text( - transactionInfo["note"] as String, - style: STextStyles.itemSubtitle12(context), - ), - ], + if ((transactionInfo["note"] as String).isNotEmpty) + const SizedBox( + height: 12, + ), + if ((transactionInfo["note"] as String).isNotEmpty) + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + (coin == Coin.epicCash) ? "Local Note" : "Note", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + transactionInfo["note"] as String, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), ), - ), ], ), if (isDesktop) diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/install_theme_from_file_dialog.dart b/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/install_theme_from_file_dialog.dart index 235bb5e95..812fcc94b 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/install_theme_from_file_dialog.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/install_theme_from_file_dialog.dart @@ -164,7 +164,7 @@ class _InstallThemeFromFileDialogState ); if (mounted) { Navigator.of(context).pop(); - if (!result) { + if (!result!) { unawaited( showDialog( context: context, diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/stack_theme_card.dart b/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/stack_theme_card.dart index 0c45a68f7..003063bde 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/stack_theme_card.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/stack_theme_card.dart @@ -72,11 +72,11 @@ class _StackThemeCardState extends ConsumerState { } Future _downloadPressed() async { - final result = await showLoading( + final result = (await showLoading( whileFuture: _downloadAndInstall(), context: context, message: "Downloading and installing theme...", - ); + ))!; if (mounted) { final message = result 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 59f443db9..fcacc60d4 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 @@ -20,11 +20,12 @@ import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/debug_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart'; -import 'package:stackwallet/pages/settings_views/global_settings_view/xpub_view.dart'; import 'package:stackwallet/pages/settings_views/sub_widgets/settings_list_button.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; import 'package:stackwallet/route_generator.dart'; @@ -231,7 +232,7 @@ class _WalletSettingsViewState extends ConsumerState { .mnemonic; if (mounted) { - Navigator.push( + await Navigator.push( context, RouteGenerator.getRoute( shouldUseMaterialRoute: @@ -305,6 +306,25 @@ class _WalletSettingsViewState extends ConsumerState { ); }, ), + if (coin == Coin.nano || coin == Coin.banano) + const SizedBox( + height: 8, + ), + if (coin == Coin.nano || coin == Coin.banano) + Consumer( + builder: (_, ref, __) { + return SettingsListButton( + iconAssetName: Assets.svg.eye, + title: "Change representative", + onPressed: () { + Navigator.of(context).pushNamed( + ChangeRepresentativeView.routeName, + arguments: widget.walletId, + ); + }, + ); + }, + ), const SizedBox( height: 8, ), @@ -434,18 +454,20 @@ class _EpiBoxInfoFormState extends ConsumerState { TextButton( onPressed: () async { try { - wallet.updateEpicboxConfig( + await wallet.updateEpicboxConfig( hostController.text, int.parse(portController.text), ); - showFloatingFlushBar( - context: context, - message: "Epicbox info saved!", - type: FlushBarType.success, - ); - wallet.refresh(); + if (mounted) { + await showFloatingFlushBar( + context: context, + message: "Epicbox info saved!", + type: FlushBarType.success, + ); + } + unawaited(wallet.refresh()); } catch (e) { - showFloatingFlushBar( + await showFloatingFlushBar( context: context, message: "Failed to save epicbox info: $e", type: FlushBarType.warning, diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart new file mode 100644 index 000000000..a91b0cb4e --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart @@ -0,0 +1,402 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/coins/banano/banano_wallet.dart'; +import 'package:stackwallet/services/coins/nano/nano_wallet.dart'; +import 'package:stackwallet/themes/stack_colors.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/coin_enum.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.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_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class ChangeRepresentativeView extends ConsumerStatefulWidget { + const ChangeRepresentativeView({ + Key? key, + required this.walletId, + this.clipboardInterface = const ClipboardWrapper(), + }) : super(key: key); + + final String walletId; + final ClipboardInterface clipboardInterface; + + static const String routeName = "/changeRepresentative"; + + @override + ConsumerState createState() => _XPubViewState(); +} + +class _XPubViewState extends ConsumerState { + final _textController = TextEditingController(); + final _textFocusNode = FocusNode(); + final bool isDesktop = Util.isDesktop; + + late ClipboardInterface _clipboardInterface; + + String? representative; + + Future loadRepresentative() async { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(widget.walletId); + + if (manager.coin == Coin.nano) { + return (manager.wallet as NanoWallet).getCurrentRepresentative(); + } else if (manager.coin == Coin.banano) { + return (manager.wallet as BananoWallet).getCurrentRepresentative(); + } + throw Exception("Unsupported wallet attempted to show representative!"); + } + + Future _save() async { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(widget.walletId); + + final changeFuture = manager.coin == Coin.nano + ? (manager.wallet as NanoWallet).changeRepresentative + : (manager.wallet as BananoWallet).changeRepresentative; + + final result = await showLoading( + whileFuture: changeFuture(_textController.text), + context: context, + message: "Updating representative...", + isDesktop: Util.isDesktop, + onException: (ex) { + String msg = ex.toString(); + while (msg.isNotEmpty && msg.startsWith("Exception:")) { + msg = msg.substring(10).trim(); + } + showFloatingFlushBar( + type: FlushBarType.warning, + message: msg, + context: context, + ); + }); + + if (mounted) { + if (result != null && result) { + setState(() { + representative = _textController.text; + _textController.text = ""; + }); + await showFloatingFlushBar( + type: FlushBarType.success, + message: "Representative changed", + context: context, + ); + } + } + } + + @override + void initState() { + _clipboardInterface = widget.clipboardInterface; + + super.initState(); + } + + @override + void dispose() { + _textController.dispose(); + _textFocusNode.dispose(); + super.dispose(); + } + + Future _copy() async { + await _clipboardInterface + .setData(ClipboardData(text: representative ?? "")); + if (mounted) { + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + )); + } + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: SafeArea( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Wallet representative", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.all(10), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + color: Theme.of(context) + .extension()! + .background, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.copy, + width: 24, + height: 24, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: () { + if (representative != null) { + _copy(); + } + }, + ), + ), + ), + ], + ), + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 16, + right: 16, + ), + child: child, + ), + ), + ), + ), + child: ConditionalParent( + condition: isDesktop, + builder: (child) => DesktopDialog( + maxWidth: 600, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Change representative", + style: STextStyles.desktopH2(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + AnimatedSize( + duration: const Duration( + milliseconds: 150, + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(32, 0, 32, 32), + child: child, + ), + ), + ], + ), + ), + child: Column( + children: [ + if (isDesktop) const SizedBox(height: 24), + ConditionalParent( + condition: !isDesktop, + builder: (child) => Expanded( + child: child, + ), + child: FutureBuilder( + future: loadRepresentative(), + builder: (context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + representative = snapshot.data!; + } + + const height = 600.0; + Widget child; + if (representative == null) { + child = const SizedBox( + key: Key("loadingRepresentative"), + height: height, + child: Center( + child: LoadingIndicator( + width: 100, + ), + ), + ); + } else { + child = Column( + children: [ + ConditionalParent( + condition: !isDesktop, + builder: (child) => RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + child, + ], + ), + ), + child: ConditionalParent( + condition: isDesktop, + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Current representative", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + const SizedBox( + height: 4, + ), + Row( + children: [ + child, + ], + ), + ], + ), + child: SelectableText( + representative!, + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), + ), + ), + ), + const SizedBox( + height: 24, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _textController, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + focusNode: _textFocusNode, + decoration: standardInputDecoration( + "Enter new representative", + _textFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + contentPadding: isDesktop + ? const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ) + : null, + suffixIcon: _textController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _textController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + if (isDesktop) const SizedBox(height: 60), + if (!isDesktop) const Spacer(), + PrimaryButton( + label: "Save", + onPressed: _save, + ), + if (!isDesktop) + const SizedBox( + height: 16, + ), + ], + ); + } + + return AnimatedSwitcher( + duration: const Duration( + milliseconds: 200, + ), + child: child, + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/global_settings_view/xpub_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart similarity index 100% rename from lib/pages/settings_views/global_settings_view/xpub_view.dart rename to lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index 31dc2b085..323ddcf8b 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -100,7 +100,7 @@ class _MyTokenSelectItemState extends ConsumerState { message: "Loading ${widget.token.name}", ); - if (!success) { + if (!success!) { return; } diff --git a/lib/pages/wallet_view/sub_widgets/wallet_summary.dart b/lib/pages/wallet_view/sub_widgets/wallet_summary.dart index a82145817..08f5f22c8 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_summary.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_summary.dart @@ -52,6 +52,7 @@ class WalletSummary extends StatelessWidget { walletId: walletId, width: constraints.maxWidth, height: constraints.maxHeight, + isFavorite: false, ), Positioned.fill( child: Padding( diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index e8ccf98de..2b1ba62c4 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -358,7 +358,6 @@ class _TransactionDetailsViewState final currentHeight = ref.watch(walletsChangeNotifierProvider .select((value) => value.getManager(walletId).currentHeight)); - print("THIS TRANSACTION IS $_transaction"); return ConditionalParent( @@ -474,7 +473,9 @@ class _TransactionDetailsViewState ), SelectableText( _transaction.isCancelled - ? "Cancelled" + ? coin == Coin.ethereum + ? "Failed" + : "Cancelled" : whatIsIt( _transaction, currentHeight, @@ -585,7 +586,9 @@ class _TransactionDetailsViewState // child: SelectableText( _transaction.isCancelled - ? "Cancelled" + ? coin == Coin.ethereum + ? "Failed" + : "Cancelled" : whatIsIt( _transaction, currentHeight, @@ -781,8 +784,8 @@ class _TransactionDetailsViewState isDesktop ? const _Divider() : const SizedBox( - height: 12, - ), + height: 12, + ), if (coin == Coin.epicCash) RoundedWhiteContainer( padding: isDesktop @@ -790,22 +793,22 @@ class _TransactionDetailsViewState : const EdgeInsets.all(12), child: Row( mainAxisAlignment: - MainAxisAlignment.spaceBetween, + MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: - CrossAxisAlignment.start, + CrossAxisAlignment.start, children: [ Text( "On chain note", style: isDesktop ? STextStyles - .desktopTextExtraExtraSmall( - context) + .desktopTextExtraExtraSmall( + context) : STextStyles.itemSubtitle( - context), + context), ), const SizedBox( height: 8, @@ -814,18 +817,16 @@ class _TransactionDetailsViewState _transaction.otherData ?? "", style: isDesktop ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of( - context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles - .itemSubtitle12( - context), + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), ), ], ), @@ -854,7 +855,9 @@ class _TransactionDetailsViewState MainAxisAlignment.spaceBetween, children: [ Text( - (coin == Coin.epicCash) ? "Local Note" : "Note ", + (coin == Coin.epicCash) + ? "Local Note" + : "Note ", style: isDesktop ? STextStyles .desktopTextExtraExtraSmall( @@ -923,7 +926,9 @@ class _TransactionDetailsViewState notesServiceChangeNotifierProvider( walletId) .select((value) => value.getNoteFor( - txid: (coin == Coin.epicCash)? _transaction.slateId! : _transaction.txid ))), + txid: (coin == Coin.epicCash) + ? _transaction.slateId! + : _transaction.txid))), builder: (builderContext, AsyncSnapshot snapshot) { if (snapshot.connectionState == diff --git a/lib/pages/wallets_view/sub_widgets/favorite_card.dart b/lib/pages/wallets_view/sub_widgets/favorite_card.dart index b3f5e97e5..107b323e0 100644 --- a/lib/pages/wallets_view/sub_widgets/favorite_card.dart +++ b/lib/pages/wallets_view/sub_widgets/favorite_card.dart @@ -149,6 +149,7 @@ class _FavoriteCardState extends ConsumerState { walletId: widget.walletId, width: widget.width, height: widget.height, + isFavorite: true, ), child: Padding( padding: const EdgeInsets.all(12.0), diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart index 68fe09aa8..61c61a807 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart @@ -13,7 +13,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/pages/settings_views/global_settings_view/xpub_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart'; import 'package:stackwallet/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart'; import 'package:stackwallet/providers/providers.dart'; @@ -21,11 +22,13 @@ import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; enum _WalletOptions { addressList, deleteWallet, + changeRepresentative, showXpub; String get prettyName { @@ -34,6 +37,8 @@ enum _WalletOptions { return "Address list"; case _WalletOptions.deleteWallet: return "Delete wallet"; + case _WalletOptions.changeRepresentative: + return "Change representative"; case _WalletOptions.showXpub: return "Show xPub"; } @@ -70,6 +75,9 @@ class WalletOptionsButton extends StatelessWidget { onAddressListPressed: () async { Navigator.of(context).pop(_WalletOptions.addressList); }, + onChangeRepPressed: () async { + Navigator.of(context).pop(_WalletOptions.changeRepresentative); + }, onShowXpubPressed: () async { Navigator.of(context).pop(_WalletOptions.showXpub); }, @@ -134,6 +142,32 @@ class WalletOptionsButton extends StatelessWidget { ), ); + if (result == true) { + if (context.mounted) { + Navigator.of(context).pop(); + } + } + break; + case _WalletOptions.changeRepresentative: + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Navigator( + initialRoute: ChangeRepresentativeView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + RouteGenerator.generateRoute( + RouteSettings( + name: ChangeRepresentativeView.routeName, + arguments: walletId, + ), + ), + ]; + }, + ), + ); + if (result == true) { if (context.mounted) { Navigator.of(context).pop(); @@ -171,18 +205,24 @@ class WalletOptionsPopupMenu extends ConsumerWidget { required this.onDeletePressed, required this.onAddressListPressed, required this.onShowXpubPressed, + required this.onChangeRepPressed, required this.walletId, }) : super(key: key); final VoidCallback onDeletePressed; final VoidCallback onAddressListPressed; final VoidCallback onShowXpubPressed; + final VoidCallback onChangeRepPressed; final String walletId; @override Widget build(BuildContext context, WidgetRef ref) { - final bool xpubEnabled = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(walletId).hasXPub)); + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId))); + final bool xpubEnabled = manager.hasXPub; + + final bool canChangeRep = + manager.coin == Coin.nano || manager.coin == Coin.banano; return Stack( children: [ @@ -237,6 +277,43 @@ class WalletOptionsPopupMenu extends ConsumerWidget { ), ), ), + if (canChangeRep) + const SizedBox( + height: 8, + ), + if (canChangeRep) + TransparentButton( + onPressed: onChangeRepPressed, + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.eye, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + const SizedBox(width: 14), + Expanded( + child: Text( + _WalletOptions.changeRepresentative.prettyName, + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ), + ], + ), + ), + ), if (xpubEnabled) const SizedBox( height: 8, diff --git a/lib/pages_desktop_specific/password/create_password_view.dart b/lib/pages_desktop_specific/password/create_password_view.dart index 68205d077..2986cd0da 100644 --- a/lib/pages_desktop_specific/password/create_password_view.dart +++ b/lib/pages_desktop_specific/password/create_password_view.dart @@ -125,7 +125,7 @@ class _CreatePasswordViewState extends ConsumerState { } } - if (!widget.restoreFromSWB) { + if (!widget.restoreFromSWB && mounted) { unawaited(showFloatingFlushBar( type: FlushBarType.success, message: "Your password is set up", diff --git a/lib/pages_desktop_specific/password/desktop_login_view.dart b/lib/pages_desktop_specific/password/desktop_login_view.dart index 10e1477b4..471da2763 100644 --- a/lib/pages_desktop_specific/password/desktop_login_view.dart +++ b/lib/pages_desktop_specific/password/desktop_login_view.dart @@ -84,10 +84,10 @@ class _DesktopLoginViewState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => Column( + builder: (context) => const Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, - children: const [ + children: [ LoadingIndicator( width: 200, height: 200, diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 7c0807caf..7385b857e 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -109,14 +109,15 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/support_vi import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart'; -import 'package:stackwallet/pages/settings_views/global_settings_view/xpub_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart'; import 'package:stackwallet/pages/stack_privacy_calls.dart'; import 'package:stackwallet/pages/token_view/my_tokens_view.dart'; import 'package:stackwallet/pages/token_view/token_contract_details_view.dart'; @@ -603,6 +604,20 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case ChangeRepresentativeView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => ChangeRepresentativeView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case AppearanceSettingsView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, diff --git a/lib/services/coins/banano/banano_wallet.dart b/lib/services/coins/banano/banano_wallet.dart index 8511f5599..a050b4027 100644 --- a/lib/services/coins/banano/banano_wallet.dart +++ b/lib/services/coins/banano/banano_wallet.dart @@ -14,9 +14,9 @@ import 'package:stackwallet/services/event_bus/events/global/node_connection_sta 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'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; -import 'package:stackwallet/services/mixins/coin_control_interface.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; +import 'package:stackwallet/services/nano_api.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; @@ -32,8 +32,7 @@ const int MINIMUM_CONFIRMATIONS = 1; const String DEFAULT_REPRESENTATIVE = "ban_1ka1ium4pfue3uxtntqsrib8mumxgazsjf58gidh1xeo5te3whsq8z476goo"; -class BananoWallet extends CoinServiceAPI - with WalletCache, WalletDB, CoinControlInterface { +class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB { BananoWallet({ required String walletId, required String walletName, @@ -925,4 +924,51 @@ class BananoWallet extends CoinServiceAPI ); await updateCachedChainHeight(height ?? 0); } + + Future getCurrentRepresentative() async { + final serverURI = Uri.parse(getCurrentNode().host); + final address = await currentReceivingAddress; + + final response = await NanoAPI.getAccountInfo( + server: serverURI, + representative: true, + account: address, + ); + + return response.accountInfo?.representative ?? DEFAULT_REPRESENTATIVE; + } + + Future changeRepresentative(String newRepresentative) async { + try { + final serverURI = Uri.parse(getCurrentNode().host); + final balance = this.balance.spendable.raw.toString(); + final String privateKey = await getPrivateKeyFromMnemonic(); + final address = await currentReceivingAddress; + + final response = await NanoAPI.getAccountInfo( + server: serverURI, + representative: true, + account: address, + ); + + if (response.accountInfo == null) { + throw response.exception ?? Exception("Failed to get account info"); + } + + final work = await requestWork(response.accountInfo!.frontier); + + return await NanoAPI.changeRepresentative( + server: serverURI, + accountType: NanoAccountType.BANANO, + account: address, + newRepresentative: newRepresentative, + previousBlock: response.accountInfo!.frontier, + balance: balance, + privateKey: privateKey, + work: work!, + ); + } catch (_) { + rethrow; + } + } } diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 9990406d9..62f3020e0 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -1023,6 +1023,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { final response = await EthereumAPI.getEthTransactions( address: thisAddress, firstBlock: isRescan ? 0 : firstBlock, + includeTokens: true, ); if (response.value == null) { @@ -1057,8 +1058,10 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { txFailed = true; } isIncoming = false; - } else { + } else if (checksumEthereumAddress(element.to) == thisAddress) { isIncoming = true; + } else { + continue; } //Calculate fees (GasLimit * gasPrice) diff --git a/lib/services/coins/nano/nano_wallet.dart b/lib/services/coins/nano/nano_wallet.dart index 1e783bf5f..391303675 100644 --- a/lib/services/coins/nano/nano_wallet.dart +++ b/lib/services/coins/nano/nano_wallet.dart @@ -24,9 +24,9 @@ import 'package:stackwallet/services/event_bus/events/global/node_connection_sta 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'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; -import 'package:stackwallet/services/mixins/coin_control_interface.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; +import 'package:stackwallet/services/nano_api.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; @@ -42,8 +42,7 @@ const int MINIMUM_CONFIRMATIONS = 1; const String DEFAULT_REPRESENTATIVE = "nano_38713x95zyjsqzx6nm1dsom1jmm668owkeb9913ax6nfgj15az3nu8xkx579"; -class NanoWallet extends CoinServiceAPI - with WalletCache, WalletDB, CoinControlInterface { +class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB { NanoWallet({ required String walletId, required String walletName, @@ -937,4 +936,51 @@ class NanoWallet extends CoinServiceAPI ); await updateCachedChainHeight(height ?? 0); } + + Future getCurrentRepresentative() async { + final serverURI = Uri.parse(getCurrentNode().host); + final address = await currentReceivingAddress; + + final response = await NanoAPI.getAccountInfo( + server: serverURI, + representative: true, + account: address, + ); + + return response.accountInfo?.representative ?? DEFAULT_REPRESENTATIVE; + } + + Future changeRepresentative(String newRepresentative) async { + try { + final serverURI = Uri.parse(getCurrentNode().host); + final balance = this.balance.spendable.raw.toString(); + final String privateKey = await getPrivateKeyFromMnemonic(); + final address = await currentReceivingAddress; + + final response = await NanoAPI.getAccountInfo( + server: serverURI, + representative: true, + account: address, + ); + + if (response.accountInfo == null) { + throw response.exception ?? Exception("Failed to get account info"); + } + + final work = await requestWork(response.accountInfo!.frontier); + + return await NanoAPI.changeRepresentative( + server: serverURI, + accountType: NanoAccountType.NANO, + account: address, + newRepresentative: newRepresentative, + previousBlock: response.accountInfo!.frontier, + balance: balance, + privateKey: privateKey, + work: work!, + ); + } catch (_) { + rethrow; + } + } } diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index d30b9ea27..5e95e3aa1 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -10,7 +10,6 @@ import 'dart:convert'; -import 'package:decimal/decimal.dart'; import 'package:http/http.dart'; import 'package:stackwallet/dto/ethereum/eth_token_tx_dto.dart'; import 'package:stackwallet/dto/ethereum/eth_token_tx_extra_dto.dart'; @@ -50,6 +49,7 @@ abstract class EthereumAPI { static Future>> getEthTransactions({ required String address, int firstBlock = 0, + bool includeTokens = false, }) async { try { final response = await get( @@ -67,7 +67,7 @@ abstract class EthereumAPI { for (final map in list!) { final txn = EthTxDTO.fromMap(Map.from(map as Map)); - if (txn.hasToken == 0) { + if (txn.hasToken == 0 || includeTokens) { txns.add(txn); } } @@ -76,9 +76,11 @@ abstract class EthereumAPI { null, ); } else { - throw EthApiException( - "getEthTransactions($address) response is empty but status code is " - "${response.statusCode}", + // nice that the api returns an empty body instead of being + // consistent and returning a json object with no transactions + return EthereumResponse( + [], + null, ); } } else { @@ -196,9 +198,11 @@ abstract class EthereumAPI { null, ); } else { - throw EthApiException( - "getEthTransactionNonces($txns) response is empty but status code is " - "${response.statusCode}", + // nice that the api returns an empty body instead of being + // consistent and returning a json object with no transactions + return EthereumResponse( + [], + null, ); } } else { @@ -252,13 +256,13 @@ abstract class EthereumAPI { ); } else { throw EthApiException( - "getEthTransaction($txids) response is empty but status code is " + "getEthTokenTransactionsByTxids($txids) response is empty but status code is " "${response.statusCode}", ); } } else { throw EthApiException( - "getEthTransaction($txids) failed with status code: " + "getEthTokenTransactionsByTxids($txids) failed with status code: " "${response.statusCode}", ); } @@ -269,7 +273,7 @@ abstract class EthereumAPI { ); } catch (e, s) { Logging.instance.log( - "getEthTransaction($txids): $e\n$s", + "getEthTokenTransactionsByTxids($txids): $e\n$s", level: LogLevel.Error, ); return EthereumResponse( @@ -307,9 +311,11 @@ abstract class EthereumAPI { null, ); } else { - throw EthApiException( - "getTokenTransactions($address, $tokenContractAddress) response is empty but status code is " - "${response.statusCode}", + // nice that the api returns an empty body instead of being + // consistent and returning a json object with no transactions + return EthereumResponse( + [], + null, ); } } else { @@ -424,10 +430,10 @@ abstract class EthereumAPI { final map = json["data"].first as Map; final balance = - Decimal.tryParse(map["balance"].toString()) ?? Decimal.zero; + BigInt.tryParse(map["units"].toString()) ?? BigInt.zero; return EthereumResponse( - Amount.fromDecimal(balance, fractionDigits: map["decimals"] as int), + Amount(rawValue: balance, fractionDigits: map["decimals"] as int), null, ); } else { diff --git a/lib/services/nano_api.dart b/lib/services/nano_api.dart new file mode 100644 index 000000000..3173bfbe6 --- /dev/null +++ b/lib/services/nano_api.dart @@ -0,0 +1,130 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:nanodart/nanodart.dart'; + +class NanoAPI { + static Future< + ({ + NAccountInfo? accountInfo, + Exception? exception, + })> getAccountInfo({ + required Uri server, + required bool representative, + required String account, + }) async { + NAccountInfo? accountInfo; + Exception? exception; + + try { + final response = await http.post( + server, + headers: { + "Content-Type": "application/json", + }, + body: jsonEncode({ + "action": "account_info", + "representative": "true", + "account": account, + }), + ); + + final map = jsonDecode(response.body); + + if (map is Map && map["error"] != null) { + throw Exception(map["error"].toString()); + } + + accountInfo = NAccountInfo( + frontier: map["frontier"] as String, + representative: map["representative"] as String, + ); + } on Exception catch (e) { + exception = e; + } catch (e) { + exception = Exception(e.toString()); + } + + return (accountInfo: accountInfo, exception: exception); + } + + static Future changeRepresentative({ + required Uri server, + required int accountType, + required String account, + required String newRepresentative, + required String previousBlock, + required String balance, + required String privateKey, + required String work, + }) async { + Map block = { + "type": "state", + "account": account, + "previous": previousBlock, + "representative": newRepresentative, + "balance": balance, + "link": + "0000000000000000000000000000000000000000000000000000000000000000", + "work": work, + }; + + final String hash; + + try { + hash = NanoBlocks.computeStateHash( + accountType, + account, + previousBlock, + newRepresentative, + BigInt.parse(balance), + block["link"] as String, + ); + } catch (e) { + if (e is RangeError) { + throw Exception("Invalid representative format"); + } + rethrow; + } + + final signature = NanoSignatures.signBlock(hash, privateKey); + + block["signature"] = signature; + + final map = await postBlock(server: server, block: block); + + if (map is Map && map["error"] != null) { + throw Exception(map["error"].toString()); + } + + return map["error"] == null; + } + + // TODO: GET RID OF DYNAMIC AND USED TYPED DATA + static Future postBlock({ + required Uri server, + required Map block, + }) async { + final response = await http.post( + server, + headers: { + "Content-Type": "application/json", + }, + body: jsonEncode({ + "action": "process", + "json_block": "true", + "subtype": "change", + "block": block, + }), + ); + + return jsonDecode(response.body); + } +} + +class NAccountInfo { + final String frontier; + final String representative; + + NAccountInfo({required this.frontier, required this.representative}); +} diff --git a/lib/themes/coin_card_provider.dart b/lib/themes/coin_card_provider.dart index fc84faebd..b34e9e6f1 100644 --- a/lib/themes/coin_card_provider.dart +++ b/lib/themes/coin_card_provider.dart @@ -22,3 +22,14 @@ final coinCardProvider = Provider.family((ref, coin) { return null; } }); + +final coinCardFavoritesProvider = Provider.family((ref, coin) { + final assets = ref.watch(themeAssetsProvider); + + if (assets is ThemeAssetsV3) { + return assets.coinCardFavoritesImages?[coin.mainNetVersion] ?? + assets.coinCardImages?[coin.mainNetVersion]; + } else { + return null; + } +}); diff --git a/lib/utilities/amount/amount_formatter.dart b/lib/utilities/amount/amount_formatter.dart index 6743bcd53..1ae577507 100644 --- a/lib/utilities/amount/amount_formatter.dart +++ b/lib/utilities/amount/amount_formatter.dart @@ -70,6 +70,11 @@ class AmountFormatter { String string, { EthContract? ethContract, }) { - return unit.tryParse(string, locale: locale, coin: coin); + return unit.tryParse( + string, + locale: locale, + coin: coin, + tokenContract: ethContract, + ); } } diff --git a/lib/utilities/show_loading.dart b/lib/utilities/show_loading.dart index db4437d6f..759eaa05c 100644 --- a/lib/utilities/show_loading.dart +++ b/lib/utilities/show_loading.dart @@ -12,15 +12,17 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; -Future showLoading({ +Future showLoading({ required Future whileFuture, required BuildContext context, required String message, String? subMessage, bool isDesktop = false, bool opaqueBG = false, + void Function(Exception)? onException, }) async { unawaited( showDialog( @@ -43,10 +45,24 @@ Future showLoading({ ), ); - final result = await whileFuture; + Exception? ex; + T? result; + + try { + result = await whileFuture; + } catch (e, s) { + Logging.instance.log( + "showLoading caught: $e\n$s", + level: LogLevel.Warning, + ); + ex = e is Exception ? e : Exception(e.toString()); + } if (context.mounted) { Navigator.of(context, rootNavigator: isDesktop).pop(); + if (ex != null) { + onException?.call(ex); + } } return result; diff --git a/lib/widgets/coin_card.dart b/lib/widgets/coin_card.dart index 0bebc5fd8..ebc67bb4e 100644 --- a/lib/widgets/coin_card.dart +++ b/lib/widgets/coin_card.dart @@ -25,11 +25,13 @@ class CoinCard extends ConsumerWidget { required this.walletId, required this.width, required this.height, + required this.isFavorite, }); final String walletId; final double width; final double height; + final bool isFavorite; @override Widget build(BuildContext context, WidgetRef ref) { @@ -38,7 +40,7 @@ class CoinCard extends ConsumerWidget { .select((value) => value.getManager(walletId).coin), ); - final bool hasCardImageBg = ref.watch(coinCardProvider(coin)) != null; + final bool hasCardImageBg = (isFavorite) ? ref.watch(coinCardFavoritesProvider(coin)) != null : ref.watch(coinCardProvider(coin)) != null; return Stack( children: [ @@ -54,7 +56,9 @@ class CoinCard extends ConsumerWidget { fit: BoxFit.cover, image: FileImage( File( - ref.watch(coinCardProvider(coin))!, + (isFavorite) + ? ref.watch(coinCardFavoritesProvider(coin))! + : ref.watch(coinCardProvider(coin))!, ), ), ), diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index 7791ff3a0..41b81e2da 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -230,7 +230,9 @@ class _TransactionCardState extends ConsumerState { fit: BoxFit.scaleDown, child: Text( _transaction.isCancelled - ? "Cancelled" + ? coin == Coin.ethereum + ? "Failed" + : "Cancelled" : whatIsIt( _transaction.type, coin, diff --git a/lib/widgets/wallet_card.dart b/lib/widgets/wallet_card.dart index 63b3a4a0e..09dcb0e2e 100644 --- a/lib/widgets/wallet_card.dart +++ b/lib/widgets/wallet_card.dart @@ -140,7 +140,7 @@ class SimpleWalletCard extends ConsumerWidget { isDesktop: Util.isDesktop, ); - if (!success) { + if (!success!) { // TODO: show error dialog here? Logging.instance.log( "Failed to load token wallet for $contract", diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 9b99a3480..c2c2e62ad 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -564,6 +564,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; STRIP_INSTALLED_PRODUCT = NO; SWIFT_COMPILATION_MODE = wholemodule; @@ -716,6 +717,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; STRIP_INSTALLED_PRODUCT = NO; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index a2ec33f19..96d3fee1a 100644 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ { - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" + "info": { + "version": 1, + "author": "xcode" }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} + "images": [ + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_16.png", + "scale": "1x" + }, + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "2x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "1x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_64.png", + "scale": "2x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_128.png", + "scale": "1x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "2x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "1x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "2x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "1x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_1024.png", + "scale": "2x" + } + ] +} \ No newline at end of file diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 82b6f9d9a..65564e63b 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index 13b35eba5..998750358 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 0a3f5fa40..7d6531e39 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index bdb57226d..e81fc06dc 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index f083318e0..5ff3261b8 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index 326c0e72c..6ae00a8d2 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index 2f1632cfd..1f195abc4 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/pubspec.lock b/pubspec.lock index 1ad96c7a7..e260cb994 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -226,10 +226,10 @@ packages: dependency: transitive description: name: cli_util - sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 url: "https://pub.dev" source: hosted - version: "0.3.5" + version: "0.4.0" clock: dependency: transitive description: @@ -584,10 +584,10 @@ packages: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: ce0e501cfc258907842238e4ca605e74b7fd1cdf04b3b43e86c43f3e40a1592c + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" url: "https://pub.dev" source: hosted - version: "0.11.0" + version: "0.13.1" flutter_libepiccash: dependency: "direct main" description: @@ -646,10 +646,10 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: "6777a3abb974021a39b5fdd2d46a03ca390e03903b6351f21d10e7ecc969f12d" + sha256: ba45d8cfbd778478a74696b012f33ffb6b1760c9bc531b21e2964444a4870dae url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.3.1" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -853,10 +853,10 @@ packages: dependency: transitive description: name: image - sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" + sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "4.0.17" import_sorter: dependency: "direct dev" description: @@ -1575,10 +1575,10 @@ packages: dependency: transitive description: name: universal_io - sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d" + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.2" url_launcher: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index e68cae35f..5a08f3018 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.7.15+181 +version: 1.7.16+182 environment: sdk: ">=3.0.2 <4.0.0" @@ -145,7 +145,7 @@ dev_dependencies: integration_test: sdk: flutter build_runner: ^2.1.7 - flutter_launcher_icons: ^0.11.0 + flutter_launcher_icons: ^0.13.1 hive_generator: ^2.0.0 dependency_validator: ^3.1.2 hive_test: ^1.0.1 @@ -157,7 +157,7 @@ dev_dependencies: flutter_lints: ^2.0.1 isar_generator: 3.0.5 -flutter_icons: +flutter_launcher_icons: android: true ios: true image_path: assets/icon/icon.png @@ -170,7 +170,7 @@ flutter_icons: icon_size: 48 # min:48, max:256, default: 48 macos: generate: true - image_path: assets/icon/icon.png + image_path: assets/icon/macos-icon.png flutter_native_splash: image: assets/images/splash.png