From dbcbfe342c5a52e1f7a30a7ad8d15099f20fb90e Mon Sep 17 00:00:00 2001 From: likho Date: Thu, 26 Jan 2023 20:08:12 +0200 Subject: [PATCH] WIP: Add test ETH Token functionality in stack --- .../sub_widgets/my_token_select_item.dart | 8 +- .../token_view/sub_widgets/token_summary.dart | 128 ++++ .../sub_widgets/token_summary_info.dart | 298 ++++++++++ lib/pages/token_view/token_view.dart | 562 +++++------------- lib/providers/global/tokens_provider.dart | 23 - .../global/tokens_service_provider.dart | 20 - lib/route_generator.dart | 10 +- .../coins/ethereum/ethereum_wallet.dart | 93 +-- lib/services/tokens.dart | 374 ------------ .../tokens/ethereum/ethereum_token.dart | 277 +++++++-- lib/services/tokens/token_manager.dart | 130 ---- lib/services/tokens/token_service.dart | 17 +- lib/utilities/eth_commons.dart | 65 +- 13 files changed, 861 insertions(+), 1144 deletions(-) create mode 100644 lib/pages/token_view/sub_widgets/token_summary.dart create mode 100644 lib/pages/token_view/sub_widgets/token_summary_info.dart delete mode 100644 lib/providers/global/tokens_provider.dart delete mode 100644 lib/providers/global/tokens_service_provider.dart delete mode 100644 lib/services/tokens.dart delete mode 100644 lib/services/tokens/token_manager.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 40fc7a154..064949cd7 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 @@ -4,8 +4,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; -import 'package:stackwallet/pages/wallets_view/wallets_view.dart'; -import 'package:stackwallet/providers/global/tokens_provider.dart'; import 'package:stackwallet/services/tokens/ethereum/ethereum_token.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -57,13 +55,15 @@ class MyTokenSelectItem extends ConsumerWidget { final mnemonicList = ref.read(managerProvider).mnemonic; final token = EthereumToken( - contractAddress: tokenData["contractAddress"] as String, + // contractAddress: tokenData["contractAddress"] as String, + tokenData: tokenData, walletMnemonic: mnemonicList); Navigator.of(context).pushNamed( TokenView.routeName, - arguments: Tuple3( + arguments: Tuple4( walletId, + tokenData, ref .read(walletsChangeNotifierProvider) .getManagerProvider(walletId), diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart new file mode 100644 index 000000000..bac284bb0 --- /dev/null +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/pages/token_view/sub_widgets/token_summary_info.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_summary_info.dart'; +import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class TokenSummary extends StatelessWidget { + const TokenSummary({ + Key? key, + required this.walletId, + required this.managerProvider, + required this.initialSyncStatus, + this.aspectRatio = 2.0, + this.minHeight = 100.0, + this.minWidth = 200.0, + this.maxHeight = 250.0, + this.maxWidth = 400.0, + }) : super(key: key); + + final String walletId; + final ChangeNotifierProvider managerProvider; + final WalletSyncStatus initialSyncStatus; + + final double aspectRatio; + final double minHeight; + final double minWidth; + final double maxHeight; + final double maxWidth; + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: aspectRatio, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: minHeight, + minWidth: minWidth, + maxHeight: maxHeight, + maxWidth: minWidth, + ), + child: Stack( + children: [ + Consumer( + builder: (_, ref, __) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .colorForCoin(ref.watch( + managerProvider.select((value) => value.coin))), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ); + }, + ), + Positioned.fill( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer( + flex: 5, + ), + Expanded( + flex: 6, + child: SvgPicture.asset( + Assets.svg.ellipse1, + // fit: BoxFit.fitWidth, + // clipBehavior: Clip.none, + ), + ), + const SizedBox( + width: 25, + ), + ], + ), + ), + // Positioned.fill( + // child: + // Column( + // mainAxisAlignment: MainAxisAlignment.end, + // children: [ + Align( + alignment: Alignment.bottomCenter, + child: Row( + children: [ + const Spacer( + flex: 1, + ), + Expanded( + flex: 3, + child: SvgPicture.asset( + Assets.svg.ellipse2, + // fit: BoxFit.f, + // clipBehavior: Clip.none, + ), + ), + const SizedBox( + width: 13, + ), + ], + ), + ), + // ], + // ), + // ), + Positioned.fill( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: TokenSummaryInfo( + walletId: walletId, + managerProvider: managerProvider, + initialSyncStatus: initialSyncStatus, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/token_view/sub_widgets/token_summary_info.dart b/lib/pages/token_view/sub_widgets/token_summary_info.dart new file mode 100644 index 000000000..40abaebe0 --- /dev/null +++ b/lib/pages/token_view/sub_widgets/token_summary_info.dart @@ -0,0 +1,298 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/animated_text.dart'; + +class TokenSummaryInfo extends StatefulWidget { + const TokenSummaryInfo({ + Key? key, + required this.walletId, + required this.managerProvider, + required this.initialSyncStatus, + }) : super(key: key); + + final String walletId; + final ChangeNotifierProvider managerProvider; + final WalletSyncStatus initialSyncStatus; + + @override + State createState() => _TokenSummaryInfoState(); +} + +class _TokenSummaryInfoState extends State { + late final String walletId; + late final ChangeNotifierProvider managerProvider; + + void showSheet() { + showModalBottomSheet( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => WalletBalanceToggleSheet(walletId: walletId), + ); + } + + Decimal? _balanceTotalCached; + Decimal? _balanceCached; + + @override + void initState() { + walletId = widget.walletId; + managerProvider = widget.managerProvider; + super.initState(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + return Row( + children: [ + Expanded( + child: Consumer( + builder: (_, ref, __) { + final Coin coin = + ref.watch(managerProvider.select((value) => value.coin)); + final externalCalls = ref.watch(prefsChangeNotifierProvider + .select((value) => value.externalCalls)); + + Future? totalBalanceFuture; + Future? availableBalanceFuture; + if (coin == Coin.firo || coin == Coin.firoTestNet) { + final firoWallet = + ref.watch(managerProvider.select((value) => value.wallet)) + as FiroWallet; + totalBalanceFuture = firoWallet.availablePublicBalance(); + availableBalanceFuture = firoWallet.availablePrivateBalance(); + } else { + totalBalanceFuture = ref.watch( + managerProvider.select((value) => value.totalBalance)); + + availableBalanceFuture = ref.watch( + managerProvider.select((value) => value.availableBalance)); + } + + final locale = ref.watch(localeServiceChangeNotifierProvider + .select((value) => value.locale)); + + final baseCurrency = ref.watch(prefsChangeNotifierProvider + .select((value) => value.currency)); + + final priceTuple = ref.watch(priceAnd24hChangeNotifierProvider + .select((value) => value.getPrice(coin))); + + final _showAvailable = + ref.watch(walletBalanceToggleStateProvider.state).state == + WalletBalanceToggleState.available; + + return FutureBuilder( + future: _showAvailable + ? availableBalanceFuture + : totalBalanceFuture, + builder: (fbContext, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData && + snapshot.data != null) { + if (_showAvailable) { + _balanceCached = snapshot.data!; + } else { + _balanceTotalCached = snapshot.data!; + } + } + Decimal? balanceToShow = + _showAvailable ? _balanceCached : _balanceTotalCached; + + if (balanceToShow != null) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: showSheet, + child: Row( + children: [ + if (coin == Coin.firo || coin == Coin.firoTestNet) + Text( + "${_showAvailable ? "Private" : "Public"} Balance", + style: + STextStyles.subtitle500(context).copyWith( + color: Theme.of(context) + .extension()! + .textFavoriteCard, + ), + ), + Text( + "${_showAvailable ? "Available" : "Full"} Balance", + style: + STextStyles.subtitle500(context).copyWith( + color: Theme.of(context) + .extension()! + .textFavoriteCard, + ), + ), + const SizedBox( + width: 4, + ), + SvgPicture.asset( + Assets.svg.chevronDown, + color: Theme.of(context) + .extension()! + .textFavoriteCard, + width: 8, + height: 4, + ), + ], + ), + ), + const Spacer(), + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "${Format.localizedStringAsFixed( + value: balanceToShow, + locale: locale, + decimalPlaces: 8, + )} ${coin.ticker}", + style: STextStyles.pageTitleH1(context).copyWith( + fontSize: 24, + color: Theme.of(context) + .extension()! + .textFavoriteCard, + ), + ), + ), + if (externalCalls) + Text( + "${Format.localizedStringAsFixed( + value: priceTuple.item1 * balanceToShow, + locale: locale, + decimalPlaces: 2, + )} $baseCurrency", + style: STextStyles.subtitle500(context).copyWith( + color: Theme.of(context) + .extension()! + .textFavoriteCard, + ), + ), + ], + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: showSheet, + child: Row( + children: [ + if (coin == Coin.firo || coin == Coin.firoTestNet) + Text( + "${_showAvailable ? "Private" : "Public"} Balance", + style: + STextStyles.subtitle500(context).copyWith( + color: Theme.of(context) + .extension()! + .textFavoriteCard, + ), + ), + if (coin != Coin.firo && coin != Coin.firoTestNet) + Text( + "${_showAvailable ? "Available" : "Full"} Balance", + style: + STextStyles.subtitle500(context).copyWith( + color: Theme.of(context) + .extension()! + .textFavoriteCard, + ), + ), + const SizedBox( + width: 4, + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: Theme.of(context) + .extension()! + .textFavoriteCard, + ), + ], + ), + ), + const Spacer(), + AnimatedText( + stringsToLoopThrough: const [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance..." + ], + style: STextStyles.pageTitleH1(context).copyWith( + fontSize: 24, + color: Theme.of(context) + .extension()! + .textFavoriteCard, + ), + ), + AnimatedText( + stringsToLoopThrough: const [ + "Loading balance", + "Loading balance.", + "Loading balance..", + "Loading balance..." + ], + style: STextStyles.subtitle500(context).copyWith( + color: Theme.of(context) + .extension()! + .textFavoriteCard, + ), + ), + ], + ); + } + }, + ); + }, + ), + ), + Column( + children: [ + Consumer( + builder: (_, ref, __) { + return SvgPicture.asset( + Assets.svg.iconFor( + coin: ref.watch( + managerProvider.select((value) => value.coin), + ), + ), + width: 24, + height: 24, + ); + }, + ), + const Spacer(), + WalletRefreshButton( + walletId: walletId, + initialSyncStatus: widget.initialSyncStatus, + ), + ], + ) + ], + ); + } +} diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 10b478606..1645cdf78 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -15,6 +15,7 @@ import 'package:stackwallet/pages/send_view/send_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/token_view/my_tokens_view.dart'; +import 'package:stackwallet/pages/token_view/sub_widgets/token_summary.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_summary.dart'; @@ -25,7 +26,6 @@ import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; import 'package:stackwallet/providers/ui/unread_notifications_provider.dart'; import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; -import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; @@ -55,6 +55,7 @@ class TokenView extends ConsumerStatefulWidget { const TokenView({ Key? key, required this.walletId, + required this.tokenData, required this.managerProvider, required this.token, this.eventBus, @@ -64,6 +65,7 @@ class TokenView extends ConsumerStatefulWidget { static const double navBarHeight = 65.0; final String walletId; + final Map tokenData; final ChangeNotifierProvider managerProvider; final EthereumToken token; final EventBus? eventBus; @@ -209,160 +211,51 @@ class _TokenViewState extends ConsumerState { } } - Widget _buildNetworkIcon(WalletSyncStatus status) { - switch (status) { - case WalletSyncStatus.unableToSync: - return SvgPicture.asset( - Assets.svg.radioProblem, - color: Theme.of(context).extension()!.accentColorRed, - width: 20, - height: 20, - ); - case WalletSyncStatus.synced: - return SvgPicture.asset( - Assets.svg.radio, - color: Theme.of(context).extension()!.accentColorGreen, - width: 20, - height: 20, - ); - case WalletSyncStatus.syncing: - return SvgPicture.asset( - Assets.svg.radioSyncing, - color: Theme.of(context).extension()!.accentColorYellow, - width: 20, - height: 20, - ); - } - } - void _onExchangePressed(BuildContext context) async { unawaited(_cnLoadingService.loadAll(ref)); final coin = ref.read(managerProvider).coin; - if (coin == Coin.epicCash) { - await showDialog( - context: context, - builder: (_) => const StackOkDialog( - title: "Exchange not available for Epic Cash", - ), - ); - } else if (coin.name.endsWith("TestNet")) { - await showDialog( - context: context, - builder: (_) => const StackOkDialog( - title: "Exchange not available for test net coins", - ), - ); - } else { - ref.read(currentExchangeNameStateProvider.state).state = - ChangeNowExchange.exchangeName; - final walletId = ref.read(managerProvider).walletId; - ref.read(prefsChangeNotifierProvider).exchangeRateType = - ExchangeRateType.estimated; + ref.read(currentExchangeNameStateProvider.state).state = + ChangeNowExchange.exchangeName; + final walletId = ref.read(managerProvider).walletId; + ref.read(prefsChangeNotifierProvider).exchangeRateType = + ExchangeRateType.estimated; - ref.read(exchangeFormStateProvider).exchange = ref.read(exchangeProvider); - ref.read(exchangeFormStateProvider).exchangeType = - ExchangeRateType.estimated; + ref.read(exchangeFormStateProvider).exchange = ref.read(exchangeProvider); + ref.read(exchangeFormStateProvider).exchangeType = + ExchangeRateType.estimated; - final currencies = ref - .read(availableChangeNowCurrenciesProvider) - .currencies - .where((element) => - element.ticker.toLowerCase() == coin.ticker.toLowerCase()); + final currencies = ref + .read(availableChangeNowCurrenciesProvider) + .currencies + .where((element) => + element.ticker.toLowerCase() == coin.ticker.toLowerCase()); - if (currencies.isNotEmpty) { - ref.read(exchangeFormStateProvider).setCurrencies( - currencies.first, - ref - .read(availableChangeNowCurrenciesProvider) - .currencies - .firstWhere( - (element) => - element.ticker.toLowerCase() != - coin.ticker.toLowerCase(), - ), - ); - } - - if (mounted) { - unawaited( - Navigator.of(context).pushNamed( - WalletInitiatedExchangeView.routeName, - arguments: Tuple3( - walletId, - coin, - _loadCNData, - ), - ), - ); - } - } - } - - Future attemptAnonymize() async { - bool shouldPop = false; - unawaited( - showDialog( - context: context, - builder: (context) => WillPopScope( - child: const CustomLoadingOverlay( - message: "Anonymizing balance", - eventBus: null, - ), - onWillPop: () async => shouldPop, - ), - ), - ); - final firoWallet = ref.read(managerProvider).wallet as FiroWallet; - - final publicBalance = await firoWallet.availablePublicBalance(); - if (publicBalance <= Decimal.zero) { - shouldPop = true; - if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName(TokenView.routeName), - ); - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "No funds available to anonymize!", - context: context, - ), - ); - } - return; + if (currencies.isNotEmpty) { + ref.read(exchangeFormStateProvider).setCurrencies( + currencies.first, + ref + .read(availableChangeNowCurrenciesProvider) + .currencies + .firstWhere( + (element) => + element.ticker.toLowerCase() != coin.ticker.toLowerCase(), + ), + ); } - try { - await firoWallet.anonymizeAllPublicFunds(); - shouldPop = true; - if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName(TokenView.routeName), - ); - unawaited( - showFloatingFlushBar( - type: FlushBarType.success, - message: "Anonymize transaction submitted", - context: context, + if (mounted) { + unawaited( + Navigator.of(context).pushNamed( + WalletInitiatedExchangeView.routeName, + arguments: Tuple3( + walletId, + coin, + _loadCNData, ), - ); - } - } catch (e) { - shouldPop = true; - if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName(TokenView.routeName), - ); - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Anonymize all failed", - message: "Reason: $e", - ), - ); - } + ), + ); } } @@ -379,6 +272,8 @@ class _TokenViewState extends ConsumerState { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); + widget.token.initializeExisting(); + // print("MY TOTAL BALANCE IS ${widget.token.totalBalance}"); final coin = ref.watch(managerProvider.select((value) => value.coin)); @@ -407,148 +302,23 @@ class _TokenViewState extends ConsumerState { const SizedBox( width: 16, ), + Expanded( + child: Text( + widget.tokenData["name"] as String, + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ), Expanded( child: Text( ref.watch( - managerProvider.select((value) => value.walletName)), + managerProvider.select((value) => value.coin.ticker)), style: STextStyles.navBarTitle(context), overflow: TextOverflow.ellipsis, ), ) ], ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("TokenViewRadioButton"), - size: 36, - shadows: const [], - color: - Theme.of(context).extension()!.background, - icon: _buildNetworkIcon(_currentSyncStatus), - onPressed: () { - Navigator.of(context).pushNamed( - WalletNetworkSettingsView.routeName, - arguments: Tuple3( - walletId, - _currentSyncStatus, - _currentNodeStatus, - ), - ); - }, - ), - ), - ), - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("TokenViewAlertsButton"), - size: 36, - shadows: const [], - color: - Theme.of(context).extension()!.background, - icon: SvgPicture.asset( - ref.watch(notificationsProvider.select((value) => - value.hasUnreadNotificationsFor(walletId))) - ? Assets.svg.bellNew(context) - : Assets.svg.bell, - width: 20, - height: 20, - color: ref.watch(notificationsProvider.select((value) => - value.hasUnreadNotificationsFor(walletId))) - ? null - : Theme.of(context) - .extension()! - .topNavIconPrimary, - ), - onPressed: () { - // reset unread state - ref.refresh(unreadNotificationsStateProvider); - - Navigator.of(context) - .pushNamed( - NotificationsView.routeName, - arguments: walletId, - ) - .then((_) { - final Set unreadNotificationIds = ref - .read(unreadNotificationsStateProvider.state) - .state; - if (unreadNotificationIds.isEmpty) return; - - List> futures = []; - for (int i = 0; - i < unreadNotificationIds.length - 1; - i++) { - futures.add(ref - .read(notificationsProvider) - .markAsRead( - unreadNotificationIds.elementAt(i), false)); - } - - // wait for multiple to update if any - Future.wait(futures).then((_) { - // only notify listeners once - ref - .read(notificationsProvider) - .markAsRead(unreadNotificationIds.last, true); - }); - }); - }, - ), - ), - ), - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("TokenViewSettingsButton"), - size: 36, - shadows: const [], - color: - Theme.of(context).extension()!.background, - icon: SvgPicture.asset( - Assets.svg.bars, - color: Theme.of(context) - .extension()! - .accentColorDark, - width: 20, - height: 20, - ), - onPressed: () { - debugPrint("wallet view settings tapped"); - Navigator.of(context).pushNamed( - WalletSettingsView.routeName, - arguments: Tuple4( - walletId, - ref.read(managerProvider).coin, - _currentSyncStatus, - _currentNodeStatus, - ), - ); - }, - ), - ), - ), - ], ), body: SafeArea( child: Container( @@ -561,7 +331,7 @@ class _TokenViewState extends ConsumerState { Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: WalletSummary( + child: TokenSummary( walletId: walletId, managerProvider: managerProvider, initialSyncStatus: ref.watch(managerProvider @@ -571,72 +341,6 @@ class _TokenViewState extends ConsumerState { ), ), ), - if (coin == Coin.firo) - const SizedBox( - height: 10, - ), - if (coin == Coin.firo) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - Expanded( - child: TextButton( - style: Theme.of(context) - .extension()! - .getSecondaryEnabledButtonColor(context), - onPressed: () async { - await showDialog( - context: context, - builder: (context) => StackDialog( - title: "Attention!", - message: - "You're about to anonymize all of your public funds.", - leftButton: TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - "Cancel", - style: STextStyles.button(context) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, - ), - ), - ), - rightButton: TextButton( - onPressed: () async { - Navigator.of(context).pop(); - - unawaited(attemptAnonymize()); - }, - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonColor( - context), - child: Text( - "Continue", - style: STextStyles.button(context), - ), - ), - ), - ); - }, - child: Text( - "Anonymize funds", - style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, - ), - ), - ), - ), - ], - ), - ), const SizedBox( height: 20, ), @@ -715,91 +419,91 @@ class _TokenViewState extends ConsumerState { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Padding( - padding: const EdgeInsets.only( - bottom: 14, - left: 16, - right: 16, - ), - child: SizedBox( - height: TokenView.navBarHeight, - child: WalletNavigationBar( - enableExchange: - Constants.enableExchange && - ref.watch(managerProvider.select( - (value) => value.coin)) != - Coin.epicCash, - height: TokenView.navBarHeight, - onExchangePressed: () => - _onExchangePressed(context), - onReceivePressed: () async { - final coin = - ref.read(managerProvider).coin; - if (mounted) { - unawaited( - Navigator.of(context).pushNamed( - ReceiveView.routeName, - arguments: Tuple2( - walletId, - coin, - ), - )); - } - }, - onSendPressed: () { - final walletId = - ref.read(managerProvider).walletId; - final coin = - ref.read(managerProvider).coin; - switch (ref - .read( - walletBalanceToggleStateProvider - .state) - .state) { - case WalletBalanceToggleState.full: - ref - .read( - publicPrivateBalanceStateProvider - .state) - .state = "Public"; - break; - case WalletBalanceToggleState - .available: - ref - .read( - publicPrivateBalanceStateProvider - .state) - .state = "Private"; - break; - } - Navigator.of(context).pushNamed( - SendView.routeName, - arguments: Tuple2( - walletId, - coin, - ), - ); - }, - onBuyPressed: () {}, - onTokensPressed: () async { - final walletAddress = await ref - .read(managerProvider) - .currentReceivingAddress; - - List tokens = - await getWalletTokens(await ref - .read(managerProvider) - .currentReceivingAddress); - - await Navigator.of(context).pushNamed( - MyTokensView.routeName, - arguments: Tuple4(managerProvider, - walletId, walletAddress, tokens), - ); - }, - ), - ), - ), + // Padding( + // padding: const EdgeInsets.only( + // bottom: 14, + // left: 16, + // right: 16, + // ), + // child: SizedBox( + // height: TokenView.navBarHeight, + // child: WalletNavigationBar( + // enableExchange: + // Constants.enableExchange && + // ref.watch(managerProvider.select( + // (value) => value.coin)) != + // Coin.epicCash, + // height: TokenView.navBarHeight, + // onExchangePressed: () => + // _onExchangePressed(context), + // onReceivePressed: () async { + // final coin = + // ref.read(managerProvider).coin; + // if (mounted) { + // unawaited( + // Navigator.of(context).pushNamed( + // ReceiveView.routeName, + // arguments: Tuple2( + // walletId, + // coin, + // ), + // )); + // } + // }, + // onSendPressed: () { + // final walletId = + // ref.read(managerProvider).walletId; + // final coin = + // ref.read(managerProvider).coin; + // switch (ref + // .read( + // walletBalanceToggleStateProvider + // .state) + // .state) { + // case WalletBalanceToggleState.full: + // ref + // .read( + // publicPrivateBalanceStateProvider + // .state) + // .state = "Public"; + // break; + // case WalletBalanceToggleState + // .available: + // ref + // .read( + // publicPrivateBalanceStateProvider + // .state) + // .state = "Private"; + // break; + // } + // Navigator.of(context).pushNamed( + // SendView.routeName, + // arguments: Tuple2( + // walletId, + // coin, + // ), + // ); + // }, + // onBuyPressed: () {}, + // onTokensPressed: () async { + // final walletAddress = await ref + // .read(managerProvider) + // .currentReceivingAddress; + // + // List tokens = + // await getWalletTokens(await ref + // .read(managerProvider) + // .currentReceivingAddress); + // + // await Navigator.of(context).pushNamed( + // MyTokensView.routeName, + // arguments: Tuple4(managerProvider, + // walletId, walletAddress, tokens), + // ); + // }, + // ), + // ), + // ), ], ), ], diff --git a/lib/providers/global/tokens_provider.dart b/lib/providers/global/tokens_provider.dart deleted file mode 100644 index 388c45971..000000000 --- a/lib/providers/global/tokens_provider.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/providers/global/node_service_provider.dart'; -import 'package:stackwallet/providers/global/tokens_service_provider.dart'; -import 'package:stackwallet/providers/global/wallets_service_provider.dart'; -import 'package:stackwallet/services/tokens.dart'; -import 'package:stackwallet/services/wallets.dart'; - -int _count = 0; - -final tokensChangeNotifierProvider = ChangeNotifierProvider((ref) { - if (kDebugMode) { - _count++; - debugPrint("tokensChangeNotifierProvider instantiation count: $_count"); - } - - final tokensService = ref.read(tokensServiceChangeNotifierProvider); - // final nodeService = ref.read(nodeServiceChangeNotifierProvider); - - final tokens = Tokens.sharedInstance; - tokens.tokensService = tokensService; - return tokens; -}); diff --git a/lib/providers/global/tokens_service_provider.dart b/lib/providers/global/tokens_service_provider.dart deleted file mode 100644 index 053bc035e..000000000 --- a/lib/providers/global/tokens_service_provider.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/providers/global/secure_store_provider.dart'; -import 'package:stackwallet/services/tokens_service.dart'; -import 'package:stackwallet/services/wallets_service.dart'; - -int _count = 0; - -final tokensServiceChangeNotifierProvider = - ChangeNotifierProvider((ref) { - if (kDebugMode) { - _count++; - debugPrint( - "tokensServiceChangeNotifierProvider instantiation count: $_count"); - } - - return TokensService( - secureStorageInterface: ref.read(secureStoreProvider), - ); -}); diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 04be917a3..21ee18aa3 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -118,7 +118,6 @@ import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/tokens/ethereum/ethereum_token.dart'; -import 'package:stackwallet/services/tokens/token_manager.dart'; import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:tuple/tuple.dart'; @@ -1328,14 +1327,15 @@ class RouteGenerator { // } case TokenView.routeName: - if (args - is Tuple3, EthereumToken>) { + if (args is Tuple4, + ChangeNotifierProvider, EthereumToken>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => TokenView( walletId: args.item1, - managerProvider: args.item2, - token: args.item3, + tokenData: args.item2, + managerProvider: args.item3, + token: args.item4, ), settings: RouteSettings( name: settings.name, diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 520186643..e056e334b 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -1,10 +1,8 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math'; -import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; -import "package:hex/hex.dart"; import 'package:decimal/decimal.dart'; import 'package:devicelocale/devicelocale.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; @@ -22,7 +20,6 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:web3dart/web3dart.dart'; -import 'package:web3dart/web3dart.dart' as web3; import 'package:web3dart/web3dart.dart' as Transaction; import 'package:stackwallet/models/models.dart' as models; @@ -66,30 +63,10 @@ class AddressTransaction { } } -class GasTracker { - final int code; - final Map data; - - const GasTracker({ - required this.code, - required this.data, - }); - - factory GasTracker.fromJson(Map json) { - return GasTracker( - code: json['code'] as int, - data: json['data'] as Map, - ); - } -} - class EthereumWallet extends CoinServiceAPI { NodeModel? _ethNode; final _gasLimit = 21000; - // final _blockExplorer = "https://blockscout.com/eth/mainnet/api?"; - final _blockExplorer = "https://api.etherscan.io/api?"; - final _gasTrackerUrl = "https://beaconcha.in/api/v1/execution/gasnow"; - final _hdPath = "m/44'/60'/0'/0"; + final _blockExplorer = "https://blockscout.com/eth/mainnet/api?"; @override String get walletId => _walletId; @@ -213,7 +190,8 @@ class EthereumWallet extends CoinServiceAPI { final amount = txData['recipientAmt']; final decimalAmount = Format.satoshisToAmount(amount as int, coin: Coin.ethereum); - final bigIntAmount = amountToBigInt(decimalAmount.toDouble()); + const decimal = 18; //Eth has up to 18 decimal places + final bigIntAmount = amountToBigInt(decimalAmount.toDouble(), decimal); final tx = Transaction.Transaction( to: EthereumAddress.fromHex(txData['address'] as String), @@ -227,12 +205,6 @@ class EthereumWallet extends CoinServiceAPI { return transaction; } - BigInt amountToBigInt(num amount) { - const decimal = 18; //Eth has up to 18 decimal places - final amountToSendinDecimal = amount * (pow(10, decimal)); - return BigInt.from(amountToSendinDecimal); - } - @override Future get currentReceivingAddress async { final _currentReceivingAddress = _credentials.address; @@ -243,14 +215,8 @@ class EthereumWallet extends CoinServiceAPI { @override Future estimateFeeFor(int satoshiAmount, int feeRate) async { - final gweiAmount = feeRate / (pow(10, 9)); - final fee = _gasLimit * gweiAmount; - - //Convert gwei to ETH - final feeInWei = fee * (pow(10, 9)); - final ethAmount = feeInWei / (pow(10, 18)); - return Format.decimalAmountToSatoshis( - Decimal.parse(ethAmount.toString()), coin); + final fee = estimateFee(feeRate, _gasLimit, 18); + return Format.decimalAmountToSatoshis(Decimal.parse(fee.toString()), coin); } @override @@ -266,26 +232,7 @@ class EthereumWallet extends CoinServiceAPI { Future? _feeObject; Future _getFees() async { - GasTracker fees = await getGasOracle(); - final feesMap = fees.data; - return FeeObject( - numberOfBlocksFast: 1, - numberOfBlocksAverage: 3, - numberOfBlocksSlow: 3, - fast: feesMap['fast'] as int, - medium: feesMap['standard'] as int, - slow: feesMap['slow'] as int); - } - - Future getGasOracle() async { - final response = await get(Uri.parse(_gasTrackerUrl)); - - if (response.statusCode == 200) { - return GasTracker.fromJson( - json.decode(response.body) as Map); - } else { - throw Exception('Failed to load gas oracle'); - } + return await getFees(); } //Full rescan is not needed for ETH since we have a balance @@ -331,20 +278,6 @@ class EthereumWallet extends CoinServiceAPI { } } - String getPrivateKey(String mnemonic) { - final isValidMnemonic = bip39.validateMnemonic(mnemonic); - if (!isValidMnemonic) { - throw 'Invalid mnemonic'; - } - - final seed = bip39.mnemonicToSeed(mnemonic); - final root = bip32.BIP32.fromSeed(seed); - const index = 0; - final addressAtIndex = root.derivePath("$_hdPath/$index"); - - return HEX.encode(addressAtIndex.privateKey as List); - } - @override Future initializeNew() async { await _prefs.init(); @@ -467,15 +400,11 @@ class EthereumWallet extends CoinServiceAPI { isSendAll = true; } - print("SATOSHI AMOUNT BEFORE $satoshiAmount"); - print("FEE IS $fee"); if (isSendAll) { //Subtract fee amount from send amount satoshiAmount -= feeEstimate; } - print("SATOSHI AMOUNT AFTER $satoshiAmount"); - Map txData = { "fee": feeEstimate, "feeInWei": fee, @@ -507,7 +436,6 @@ class EthereumWallet extends CoinServiceAPI { String privateKey = getPrivateKey(mnemonic); _credentials = EthPrivateKey.fromHex(privateKey); - // print(_credentials.address); //Get ERC-20 transactions for wallet (So we can get the and save wallet's ERC-20 TOKENS AddressTransaction tokenTransactions = await fetchAddressTransactions( _credentials.address.toString(), "tokentx"); @@ -515,7 +443,7 @@ class EthereumWallet extends CoinServiceAPI { List> tokensList = []; if (tokenTransactions.message == "OK") { final allTxs = tokenTransactions.result; - print("RESULT IS $allTxs"); + allTxs.forEach((element) { String key = element["tokenSymbol"] as String; tokenMap[key] = {}; @@ -544,9 +472,6 @@ class EthereumWallet extends CoinServiceAPI { key: '${_walletId}_tokens', value: tokensList.toString()); } - print("THIS WALLET TOKENS IS $tokenMap"); - print("ALL TOKENS LIST IS $tokensList"); - await DB.instance .put(boxName: walletId, key: "id", value: _walletId); await DB.instance @@ -976,7 +901,9 @@ class EthereumWallet extends CoinServiceAPI { if (checksumEthereumAddress(element["from"].toString()) == thisAddress) { - midSortedTx["txType"] = "Sent"; + midSortedTx["txType"] = (int.parse(element["isError"] as String) == 0) + ? "Sent" + : "Send Failed"; } else { midSortedTx["txType"] = "Received"; } diff --git a/lib/services/tokens.dart b/lib/services/tokens.dart deleted file mode 100644 index d7997d9ed..000000000 --- a/lib/services/tokens.dart +++ /dev/null @@ -1,374 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/hive/db.dart'; -import 'package:stackwallet/models/node_model.dart'; -import 'package:stackwallet/services/coins/coin_service.dart'; -import 'package:stackwallet/services/coins/manager.dart'; -import 'package:stackwallet/services/node_service.dart'; -import 'package:stackwallet/services/tokens/token_manager.dart'; -import 'package:stackwallet/services/tokens_service.dart'; -import 'package:stackwallet/services/transaction_notification_tracker.dart'; -import 'package:stackwallet/services/wallets_service.dart'; -import 'package:stackwallet/utilities/default_nodes.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; -import 'package:stackwallet/utilities/listenable_list.dart'; -import 'package:stackwallet/utilities/listenable_map.dart'; -import 'package:stackwallet/utilities/logger.dart'; -import 'package:stackwallet/utilities/prefs.dart'; -import 'package:tuple/tuple.dart'; - -// final ListenableList> _nonFavorites = -// ListenableList(); -// ListenableList> get nonFavorites => -// _nonFavorites; -// -// final ListenableList> _favorites = -// ListenableList(); -// ListenableList> get favorites => _favorites; - -class Tokens extends ChangeNotifier { - Tokens._private(); - - @override - dispose() { - debugPrint("Tokens dispose was called!!"); - super.dispose(); - } - - static final Tokens _sharedInstance = Tokens._private(); - static Tokens get sharedInstance => _sharedInstance; - - late TokensService tokensService; - // late NodeService nodeService; - - // mirrored maps for access to reading managers without using riverpod ref - static final ListenableMap> - _managerProviderMap = ListenableMap(); - static final ListenableMap _managerMap = - ListenableMap(); - - // bool get hasWallets => _managerProviderMap.isNotEmpty; - - List> get managerProviders => - _managerProviderMap.values.toList(growable: false); - List get managers => _managerMap.values.toList(growable: false); - - // List getWalletIdsFor({required Coin coin}) { - // final List result = []; - // for (final manager in _managerMap.values) { - // if (manager.coin == coin) { - // result.add(manager.walletId); - // } - // } - // return result; - // } - - // Map>> getManagerProvidersByCoin() { - // print("DOES THIS GET HERE?????"); - // Map>> result = {}; - // for (final manager in _managerMap.values) { - // if (result[manager.coin] == null) { - // result[manager.coin] = []; - // } - // result[manager.coin]!.add(_managerProviderMap[manager.walletId] - // as ChangeNotifierProvider); - // } - // return result; - // } - - // List> getManagerProvidersForCoin(Coin coin) { - // List> result = []; - // for (final manager in _managerMap.values) { - // if (manager.coin == coin) { - // result.add(_managerProviderMap[manager.walletId] - // as ChangeNotifierProvider); - // } - // } - // return result; - // } - - ChangeNotifierProvider getManagerProvider( - String contractAddress) { - print("WALLET ID HERE IS ${_managerProviderMap.length}"); - return _managerProviderMap[contractAddress] - as ChangeNotifierProvider; - } - - TokenManager getManager(String contractAddress) { - return _managerMap[contractAddress] as TokenManager; - } - - void addToken( - {required String contractAddress, required TokenManager manager}) { - _managerMap.add(contractAddress, manager, true); - _managerProviderMap.add(contractAddress, - ChangeNotifierProvider((_) => manager), true); - - notifyListeners(); - } - // - // void removeWallet({required String walletId}) { - // if (_managerProviderMap[walletId] == null) { - // Logging.instance.log( - // "Wallets.removeWallet($walletId) failed. ManagerProvider with $walletId not found!", - // level: LogLevel.Warning); - // return; - // } - // - // final provider = _managerProviderMap[walletId]!; - // - // // in both non and favorites for removal - // _favorites.remove(provider, true); - // _nonFavorites.remove(provider, true); - // - // _managerProviderMap.remove(walletId, true); - // _managerMap.remove(walletId, true)!.exitCurrentWallet(); - // - // notifyListeners(); - // } - - static bool hasLoaded = false; - - Future _initLinearly( - List> tuples, - ) async { - for (final tuple in tuples) { - await tuple.item1.initializeExisting(); - if (tuple.item2 && !tuple.item1.shouldAutoSync) { - tuple.item1.shouldAutoSync = true; - } - } - } - - static int _count = 0; - Future load(Prefs prefs) async { - debugPrint("++++++++++++++ Tokens().load() called: ${++_count} times"); - if (hasLoaded) { - return; - } - hasLoaded = true; - - // clear out any wallet hive boxes where the wallet was deleted in previous app run - // for (final walletId in DB.instance - // .values(boxName: DB.boxNameWalletsToDeleteOnStart)) { - // await DB.instance.deleteBoxFromDisk(boxName: walletId); - // } - // // clear list - // await DB.instance - // .deleteAll(boxName: DB.boxNameWalletsToDeleteOnStart); - // - // final map = await walletsService.walletNames; - - // List> walletInitFutures = []; - // List> walletsToInitLinearly = []; - - // final favIdList = await walletsService.getFavoriteWalletIds(); - - // List walletIdsToEnableAutoSync = []; - // bool shouldAutoSyncAll = false; - // switch (prefs.syncType) { - // case SyncingType.currentWalletOnly: - // // do nothing as this will be set when going into a wallet from the main screen - // break; - // case SyncingType.selectedWalletsAtStartup: - // walletIdsToEnableAutoSync.addAll(prefs.walletIdsSyncOnStartup); - // break; - // case SyncingType.allWalletsOnStartup: - // shouldAutoSyncAll = true; - // break; - // } - - // for (final entry in map.entries) { - // try { - // final walletId = entry.value.walletId; - // - // late final bool isVerified; - // try { - // isVerified = - // await walletsService.isMnemonicVerified(walletId: walletId); - // } catch (e, s) { - // Logging.instance.log("$e $s", level: LogLevel.Warning); - // isVerified = false; - // } - // - // Logging.instance.log( - // "LOADING WALLET: ${entry.value.toString()} IS VERIFIED: $isVerified", - // level: LogLevel.Info); - // if (isVerified) { - // if (_managerMap[walletId] == null && - // _managerProviderMap[walletId] == null) { - // final coin = entry.value.coin; - // NodeModel node = nodeService.getPrimaryNodeFor(coin: coin) ?? - // DefaultNodes.getNodeFor(coin); - // // ElectrumXNode? node = await nodeService.getCurrentNode(coin: coin); - // - // // folowing shouldn't be needed as the defaults get saved on init - // // if (node == null) { - // // node = DefaultNodes.getNodeFor(coin); - // // - // // // save default node - // // nodeService.add(node, false); - // // } - // - // final txTracker = - // TransactionNotificationTracker(walletId: walletId); - // - // final failovers = nodeService.failoverNodesFor(coin: coin); - // - // // load wallet - // final wallet = CoinServiceAPI.from( - // coin, - // walletId, - // entry.value.name, - // nodeService.secureStorageInterface, - // node, - // txTracker, - // prefs, - // failovers, - // ); - // - // final manager = Manager(wallet); - // - // final shouldSetAutoSync = shouldAutoSyncAll || - // walletIdsToEnableAutoSync.contains(manager.walletId); - // - // if (manager.coin == Coin.monero || manager.coin == Coin.wownero) { - // walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); - // } else { - // walletInitFutures.add(manager.initializeExisting().then((value) { - // if (shouldSetAutoSync) { - // manager.shouldAutoSync = true; - // } - // })); - // } - // - // _managerMap.add(walletId, manager, false); - // - // final managerProvider = - // ChangeNotifierProvider((_) => manager); - // _managerProviderMap.add(walletId, managerProvider, false); - // - // final favIndex = favIdList.indexOf(walletId); - // - // if (favIndex == -1) { - // _nonFavorites.add(managerProvider, true); - // } else { - // // it is a favorite - // if (favIndex >= _favorites.length) { - // _favorites.add(managerProvider, true); - // } else { - // _favorites.insert(favIndex, managerProvider, true); - // } - // } - // } - // } else { - // // wallet creation was not completed by user so we remove it completely - // await walletsService.deleteWallet(entry.value.name, false); - // } - // } catch (e, s) { - // Logging.instance.log("$e $s", level: LogLevel.Fatal); - // continue; - // } - // } - - // if (walletInitFutures.isNotEmpty && walletsToInitLinearly.isNotEmpty) { - // await Future.wait([ - // _initLinearly(walletsToInitLinearly), - // ...walletInitFutures, - // ]); - // notifyListeners(); - // } else if (walletInitFutures.isNotEmpty) { - // await Future.wait(walletInitFutures); - // notifyListeners(); - // } else if (walletsToInitLinearly.isNotEmpty) { - // await _initLinearly(walletsToInitLinearly); - // notifyListeners(); - // } - } - - // Future loadAfterStackRestore( - // Prefs prefs, List managers) async { - // List> walletInitFutures = []; - // List> walletsToInitLinearly = []; - // - // final favIdList = await walletsService.getFavoriteWalletIds(); - // - // List walletIdsToEnableAutoSync = []; - // bool shouldAutoSyncAll = false; - // switch (prefs.syncType) { - // case SyncingType.currentWalletOnly: - // // do nothing as this will be set when going into a wallet from the main screen - // break; - // case SyncingType.selectedWalletsAtStartup: - // walletIdsToEnableAutoSync.addAll(prefs.walletIdsSyncOnStartup); - // break; - // case SyncingType.allWalletsOnStartup: - // shouldAutoSyncAll = true; - // break; - // } - // - // for (final manager in managers) { - // final walletId = manager.walletId; - // - // final isVerified = - // await walletsService.isMnemonicVerified(walletId: walletId); - // debugPrint( - // "LOADING RESTORED WALLET: ${manager.walletName} ${manager.walletId} IS VERIFIED: $isVerified"); - // - // if (isVerified) { - // if (_managerMap[walletId] == null && - // _managerProviderMap[walletId] == null) { - // final shouldSetAutoSync = shouldAutoSyncAll || - // walletIdsToEnableAutoSync.contains(manager.walletId); - // - // if (manager.coin == Coin.monero || manager.coin == Coin.wownero) { - // walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync)); - // } else { - // walletInitFutures.add(manager.initializeExisting().then((value) { - // if (shouldSetAutoSync) { - // manager.shouldAutoSync = true; - // } - // })); - // } - // - // _managerMap.add(walletId, manager, false); - // - // final managerProvider = - // ChangeNotifierProvider((_) => manager); - // _managerProviderMap.add(walletId, managerProvider, false); - // - // final favIndex = favIdList.indexOf(walletId); - // - // if (favIndex == -1) { - // _nonFavorites.add(managerProvider, true); - // } else { - // // it is a favorite - // if (favIndex >= _favorites.length) { - // _favorites.add(managerProvider, true); - // } else { - // _favorites.insert(favIndex, managerProvider, true); - // } - // } - // } - // } else { - // // wallet creation was not completed by user so we remove it completely - // await walletsService.deleteWallet(manager.walletName, false); - // } - // } - // - // if (walletInitFutures.isNotEmpty && walletsToInitLinearly.isNotEmpty) { - // await Future.wait([ - // _initLinearly(walletsToInitLinearly), - // ...walletInitFutures, - // ]); - // notifyListeners(); - // } else if (walletInitFutures.isNotEmpty) { - // await Future.wait(walletInitFutures); - // notifyListeners(); - // } else if (walletsToInitLinearly.isNotEmpty) { - // await _initLinearly(walletsToInitLinearly); - // notifyListeners(); - // } - // } -} diff --git a/lib/services/tokens/ethereum/ethereum_token.dart b/lib/services/tokens/ethereum/ethereum_token.dart index 5ddef7259..01407aa60 100644 --- a/lib/services/tokens/ethereum/ethereum_token.dart +++ b/lib/services/tokens/ethereum/ethereum_token.dart @@ -1,16 +1,20 @@ import 'dart:convert'; - -import 'package:decimal/decimal.dart'; +import 'dart:math'; import 'package:http/http.dart'; +import 'package:decimal/decimal.dart'; +import 'package:stackwallet/utilities/eth_commons.dart'; +import 'package:ethereum_addresses/ethereum_addresses.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/models/paymint/transactions_model.dart'; -import 'package:stackwallet/services/tokens/token_service.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/eth_commons.dart'; - import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/services/tokens/token_service.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:web3dart/web3dart.dart'; +import 'package:web3dart/web3dart.dart' as transaction; class AbiRequestResponse { final String message; @@ -35,118 +39,222 @@ class AbiRequestResponse { class EthereumToken extends TokenServiceAPI { @override late bool shouldAutoSync; - late String _contractAddress; + late EthereumAddress _contractAddress; late EthPrivateKey _credentials; + late DeployedContract _contract; + late Map _tokenData; + late ContractFunction _balanceFunction; + late ContractFunction _sendFunction; late Future> _walletMnemonic; late SecureStorageInterface _secureStore; late String _tokenAbi; + late Web3Client _client; late final TransactionNotificationTracker txTracker; String rpcUrl = 'https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba'; + final _gasLimit = 200000; EthereumToken({ - required String contractAddress, + required Map tokenData, required Future> walletMnemonic, // required SecureStorageInterface secureStore, }) { - _contractAddress = contractAddress; + _contractAddress = + EthereumAddress.fromHex(tokenData["contractAddress"] as String); _walletMnemonic = walletMnemonic; + _tokenData = tokenData; // _secureStore = secureStore; } Future fetchTokenAbi() async { + print( + "$blockExplorer?module=contract&action=getabi&address=$_contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP"); final response = await get(Uri.parse( - "https://api.etherscan.io/api?module=contract&action=getabi&address=$_contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); + "$blockExplorer?module=contract&action=getabi&address=$_contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); if (response.statusCode == 200) { return AbiRequestResponse.fromJson( json.decode(response.body) as Map); } else { - throw Exception('Failed to load transactions'); + throw Exception('Failed to load token abi'); } } @override - // TODO: implement allOwnAddresses - Future> get allOwnAddresses => throw UnimplementedError(); + Future> get allOwnAddresses => + _allOwnAddresses ??= _fetchAllOwnAddresses(); + Future>? _allOwnAddresses; - @override - // TODO: implement availableBalance - Future get availableBalance => throw UnimplementedError(); - - @override - // TODO: implement balanceMinusMaxFee - Future get balanceMinusMaxFee => throw UnimplementedError(); - - @override - // TODO: implement coin - Coin get coin => throw UnimplementedError(); - - @override - Future confirmSend({required Map txData}) { - // TODO: implement confirmSend - throw UnimplementedError(); + Future> _fetchAllOwnAddresses() async { + List addresses = []; + final ownAddress = _credentials.address; + addresses.add(ownAddress.toString()); + return addresses; } @override - // TODO: implement currentReceivingAddress - Future get currentReceivingAddress => throw UnimplementedError(); - - @override - Future estimateFeeFor(int satoshiAmount, int feeRate) { - // TODO: implement estimateFeeFor - throw UnimplementedError(); + Future get availableBalance async { + return await totalBalance; } @override - // TODO: implement fees - Future get fees => throw UnimplementedError(); + Future get balanceMinusMaxFee async => + (await availableBalance) - + (Decimal.fromInt((await maxFee)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(); + + @override + Coin get coin => Coin.ethereum; + + @override + Future confirmSend({required Map txData}) async { + final amount = txData['recipientAmt']; + final decimalAmount = + Format.satoshisToAmount(amount as int, coin: Coin.ethereum); + final bigIntAmount = amountToBigInt( + decimalAmount.toDouble(), int.parse(_tokenData["decimals"] as String)); + + final sentTx = await _client.sendTransaction( + _credentials, + transaction.Transaction.callContract( + contract: _contract, + function: _sendFunction, + parameters: [ + EthereumAddress.fromHex(txData['address'] as String), + bigIntAmount + ], + maxGas: _gasLimit, + gasPrice: EtherAmount.fromUnitAndValue( + EtherUnit.wei, txData['feeInWei']))); + + return sentTx; + } + + @override + Future get currentReceivingAddress async { + final _currentReceivingAddress = await _credentials.extractAddress(); + final checkSumAddress = + checksumEthereumAddress(_currentReceivingAddress.toString()); + return checkSumAddress; + } + + @override + Future estimateFeeFor(int satoshiAmount, int feeRate) async { + final fee = estimateFee( + feeRate, _gasLimit, int.parse(_tokenData["decimals"] as String)); + return Format.decimalAmountToSatoshis(Decimal.parse(fee.toString()), coin); + } + + @override + Future get fees => _feeObject ??= _getFees(); + Future? _feeObject; + + Future _getFees() async { + return await getFees(); + } @override Future initializeExisting() async { + //TODO - GET abi FROM secure store AbiRequestResponse abi = await fetchTokenAbi(); //Fetch token ABI so we can call token functions if (abi.message == "OK") { _tokenAbi = abi.result; } - final mnemonic = await _walletMnemonic; String mnemonicString = mnemonic.join(' '); //Get private key for given mnemonic String privateKey = getPrivateKey(mnemonicString); - // TODO: implement initializeExisting - throw UnimplementedError(); + _credentials = EthPrivateKey.fromHex(privateKey); + + _contract = DeployedContract( + ContractAbi.fromJson(_tokenAbi, _tokenData["name"] as String), + _contractAddress); + _balanceFunction = _contract.function('balanceOf'); + _sendFunction = _contract.function('transfer'); + _client = await getEthClient(); + print("${await totalBalance}"); } @override Future initializeNew() async { - throw UnimplementedError(); - } + //TODO - Save abi in secure store + AbiRequestResponse abi = await fetchTokenAbi(); + //Fetch token ABI so we can call token functions + if (abi.message == "OK") { + _tokenAbi = abi.result; + } + final mnemonic = await _walletMnemonic; + String mnemonicString = mnemonic.join(' '); - @override - // TODO: implement isConnected - bool get isConnected => throw UnimplementedError(); + //Get private key for given mnemonic + String privateKey = getPrivateKey(mnemonicString); + _credentials = EthPrivateKey.fromHex(privateKey); + + _contract = DeployedContract( + ContractAbi.fromJson(_tokenAbi, _tokenData["name"] as String), + _contractAddress); + _balanceFunction = _contract.function('balanceOf'); + _sendFunction = _contract.function('transfer'); + _client = await getEthClient(); + } @override // TODO: implement isRefreshing bool get isRefreshing => throw UnimplementedError(); @override - // TODO: implement maxFee - Future get maxFee => throw UnimplementedError(); - - @override - // TODO: implement pendingBalance - Future get pendingBalance => throw UnimplementedError(); + Future get maxFee async { + final fee = (await fees).fast; + final feeEstimate = await estimateFeeFor(0, fee); + return feeEstimate; + } @override Future> prepareSend( {required String address, required int satoshiAmount, - Map? args}) { - // TODO: implement prepareSend - throw UnimplementedError(); + Map? args}) async { + final feeRateType = args?["feeRate"]; + int fee = 0; + final feeObject = await fees; + switch (feeRateType) { + case FeeRateType.fast: + fee = feeObject.fast; + break; + case FeeRateType.average: + fee = feeObject.medium; + break; + case FeeRateType.slow: + fee = feeObject.slow; + break; + } + + final feeEstimate = await estimateFeeFor(satoshiAmount, fee); + + bool isSendAll = false; + final balance = + Format.decimalAmountToSatoshis(await availableBalance, coin); + if (satoshiAmount == balance) { + isSendAll = true; + } + + if (isSendAll) { + //Subtract fee amount from send amount + satoshiAmount -= feeEstimate; + } + + Map txData = { + "fee": feeEstimate, + "feeInWei": fee, + "address": address, + "recipientAmt": satoshiAmount, + }; + + print("TX DATA TO BE SENT IS $txData"); + return txData; } @override @@ -156,22 +264,69 @@ class EthereumToken extends TokenServiceAPI { } @override - // TODO: implement totalBalance - Future get totalBalance => throw UnimplementedError(); + Future get totalBalance async { + final balanceRequest = await _client.call( + contract: _contract, + function: _balanceFunction, + params: [_credentials.address]); + + String balance = balanceRequest.first.toString(); + int tokenDecimals = int.parse(_tokenData["decimals"] as String); + final balanceInDecimal = (int.parse(balance) / (pow(10, tokenDecimals))); + return Decimal.parse(balanceInDecimal.toString()); + } @override // TODO: implement transactionData Future get transactionData => throw UnimplementedError(); @override - Future updateSentCachedTxData(Map txData) { - // TODO: implement updateSentCachedTxData - throw UnimplementedError(); + Future updateSentCachedTxData(Map txData) async { + Decimal currentPrice = Decimal.parse(0.0 as String); + final locale = await Devicelocale.currentLocale; + final String worthNow = Format.localizedStringAsFixed( + value: + ((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2), + decimalPlaces: 2, + locale: locale!); + + final tx = models.Transaction( + txid: txData["txid"] as String, + confirmedStatus: false, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + txType: "Sent", + amount: txData["recipientAmt"] as int, + worthNow: worthNow, + worthAtBlockTimestamp: worthNow, + fees: txData["fee"] as int, + inputSize: 0, + outputSize: 0, + inputs: [], + outputs: [], + address: txData["address"] as String, + height: -1, + confirmations: 0, + ); + + if (cachedTxData == null) { + final data = await _fetchTransactionData(); + _transactionData = Future(() => data); + } else { + final transactions = cachedTxData!.getAllTransactions(); + transactions[tx.txid] = tx; + cachedTxData = models.TransactionData.fromMap(transactions); + _transactionData = Future(() => cachedTxData!); + } } @override bool validateAddress(String address) { - // TODO: implement validateAddress - throw UnimplementedError(); + return isValidEthereumAddress(address); + } + + Future getEthClient() async { + return Web3Client(rpcUrl, Client()); } } diff --git a/lib/services/tokens/token_manager.dart b/lib/services/tokens/token_manager.dart deleted file mode 100644 index cceee7e47..000000000 --- a/lib/services/tokens/token_manager.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'dart:async'; - -import 'package:decimal/decimal.dart'; -import 'package:event_bus/event_bus.dart'; -import 'package:flutter/material.dart'; -import 'package:stackwallet/models/models.dart'; -import 'package:stackwallet/services/coins/coin_service.dart'; -import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; -import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; -import 'package:stackwallet/services/event_bus/global_event_bus.dart'; -import 'package:stackwallet/services/tokens/token_service.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/logger.dart'; - -class TokenManager with ChangeNotifier { - final TokenServiceAPI _currentToken; - StreamSubscription? _backgroundRefreshListener; - - /// optional eventbus parameter for testing only - TokenManager(this._currentToken, [EventBus? globalEventBusForTesting]) { - final bus = globalEventBusForTesting ?? GlobalEventBus.instance; - _backgroundRefreshListener = bus.on().listen( - (event) async { - // if (event.walletId == walletId) { - // notifyListeners(); - // Logging.instance.log( - // "UpdatedInBackgroundEvent activated notifyListeners() in Manager instance $hashCode $walletName with: ${event.message}", - // level: LogLevel.Info); - // } - }, - ); - } - - TokenServiceAPI get token => _currentToken; - - bool get hasBackgroundRefreshListener => _backgroundRefreshListener != null; - - bool get isRefreshing => _currentToken.isRefreshing; - - bool get shouldAutoSync => _currentToken.shouldAutoSync; - set shouldAutoSync(bool shouldAutoSync) => - _currentToken.shouldAutoSync = shouldAutoSync; - - Future> prepareSend({ - required String address, - required int satoshiAmount, - Map? args, - }) async { - try { - final txInfo = await _currentToken.prepareSend( - address: address, - satoshiAmount: satoshiAmount, - args: args, - ); - // notifyListeners(); - return txInfo; - } catch (e) { - // rethrow to pass error in alert - rethrow; - } - } - - Future confirmSend({required Map txData}) async { - try { - final txid = await _currentToken.confirmSend(txData: txData); - - txData["txid"] = txid; - await _currentToken.updateSentCachedTxData(txData); - - notifyListeners(); - return txid; - } catch (e) { - // rethrow to pass error in alert - rethrow; - } - } - - Future get fees => _currentToken.fees; - Future get maxFee => _currentToken.maxFee; - - Future get currentReceivingAddress => - _currentToken.currentReceivingAddress; - // Future get currentLegacyReceivingAddress => - // _currentWallet.currentLegacyReceivingAddress; - - Future get availableBalance async { - _cachedAvailableBalance = await _currentToken.availableBalance; - return _cachedAvailableBalance; - } - - Decimal _cachedAvailableBalance = Decimal.zero; - Decimal get cachedAvailableBalance => _cachedAvailableBalance; - - Future get pendingBalance => _currentToken.pendingBalance; - Future get balanceMinusMaxFee => _currentToken.balanceMinusMaxFee; - - Future get totalBalance async { - _cachedTotalBalance = await _currentToken.totalBalance; - return _cachedTotalBalance; - } - - Decimal _cachedTotalBalance = Decimal.zero; - Decimal get cachedTotalBalance => _cachedTotalBalance; - - Future> get allOwnAddresses => _currentToken.allOwnAddresses; - - Future get transactionData => _currentToken.transactionData; - - Future refresh() async { - await _currentToken.refresh(); - notifyListeners(); - } - - bool validateAddress(String address) => - _currentToken.validateAddress(address); - - Future initializeNew() => _currentToken.initializeNew(); - Future initializeExisting() => _currentToken.initializeExisting(); - - Future isOwnAddress(String address) async { - final allOwnAddresses = await this.allOwnAddresses; - return allOwnAddresses.contains(address); - } - - bool get isConnected => _currentToken.isConnected; - - Future estimateFeeFor(int satoshiAmount, int feeRate) async { - return _currentToken.estimateFeeFor(satoshiAmount, feeRate); - } -} diff --git a/lib/services/tokens/token_service.dart b/lib/services/tokens/token_service.dart index 0f23054f5..c19897b08 100644 --- a/lib/services/tokens/token_service.dart +++ b/lib/services/tokens/token_service.dart @@ -10,17 +10,17 @@ abstract class TokenServiceAPI { TokenServiceAPI(); factory TokenServiceAPI.from( - String contractAddress, - String walletId, + Map tokenData, + Future> walletMnemonic, SecureStorageInterface secureStorageInterface, TransactionNotificationTracker tracker, Prefs prefs, ) { return EthereumToken( - contractAddress: contractAddress, - walletId: walletId, - secureStore: secureStorageInterface, - tracker: tracker, + tokenData: tokenData, + walletMnemonic: walletMnemonic, + // secureStore: secureStorageInterface, + // tracker: tracker, ); } @@ -44,7 +44,6 @@ abstract class TokenServiceAPI { // Future get currentLegacyReceivingAddress; Future get availableBalance; - Future get pendingBalance; Future get totalBalance; Future get balanceMinusMaxFee; @@ -62,10 +61,6 @@ abstract class TokenServiceAPI { Future initializeNew(); Future initializeExisting(); - // void Function(bool isActive)? onIsActiveWalletChanged; - - bool get isConnected; - Future estimateFeeFor(int satoshiAmount, int feeRate); // used for electrumx coins diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart index c3c95f8a6..5a221f828 100644 --- a/lib/utilities/eth_commons.dart +++ b/lib/utilities/eth_commons.dart @@ -1,7 +1,9 @@ import 'dart:convert'; +import 'dart:math'; import 'package:http/http.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'flutter_secure_storage_interface.dart'; import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; @@ -27,13 +29,31 @@ class AccountModule { } } -const _blockExplorer = "https://api.etherscan.io/api?"; -late SecureStorageInterface _secureStore; +class GasTracker { + final int code; + final Map data; + + const GasTracker({ + required this.code, + required this.data, + }); + + factory GasTracker.fromJson(Map json) { + return GasTracker( + code: json['code'] as int, + data: json['data'] as Map, + ); + } +} + +// const blockExplorer = "https://blockscout.com/eth/mainnet/api"; +const blockExplorer = "https://api.etherscan.io/api"; const _hdPath = "m/44'/60'/0'/0"; +const _gasTrackerUrl = "https://beaconcha.in/api/v1/execution/gasnow"; Future fetchAccountModule(String action, String address) async { final response = await get(Uri.parse( - "${_blockExplorer}module=account&action=$action&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); + "${blockExplorer}module=account&action=$action&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); if (response.statusCode == 200) { return AccountModule.fromJson( json.decode(response.body) as Map); @@ -48,7 +68,6 @@ Future> getWalletTokens(String address) async { var tokenMap = {}; if (tokens.message == "OK") { final allTxs = tokens.result; - print("RESULT IS $allTxs"); allTxs.forEach((element) { String key = element["tokenSymbol"] as String; tokenMap[key] = {}; @@ -88,3 +107,41 @@ String getPrivateKey(String mnemonic) { return HEX.encode(addressAtIndex.privateKey as List); } + +Future getGasOracle() async { + final response = await get(Uri.parse(_gasTrackerUrl)); + + if (response.statusCode == 200) { + return GasTracker.fromJson( + json.decode(response.body) as Map); + } else { + throw Exception('Failed to load gas oracle'); + } +} + +Future getFees() async { + GasTracker fees = await getGasOracle(); + final feesMap = fees.data; + return FeeObject( + numberOfBlocksFast: 1, + numberOfBlocksAverage: 3, + numberOfBlocksSlow: 3, + fast: feesMap['fast'] as int, + medium: feesMap['standard'] as int, + slow: feesMap['slow'] as int); +} + +double estimateFee(int feeRate, int gasLimit, int decimals) { + final gweiAmount = feeRate / (pow(10, 9)); + final fee = gasLimit * gweiAmount; + + //Convert gwei to ETH + final feeInWei = fee * (pow(10, 9)); + final ethAmount = feeInWei / (pow(10, decimals)); + return ethAmount; +} + +BigInt amountToBigInt(num amount, int decimal) { + final amountToSendinDecimal = amount * (pow(10, decimal)); + return BigInt.from(amountToSendinDecimal); +}