From d4653ea794fdd3b261f2d7287697765ceef7957e Mon Sep 17 00:00:00 2001 From: likho Date: Wed, 25 Jan 2023 18:08:27 +0200 Subject: [PATCH] WIP: Add token functionality --- .../sub_widgets/my_token_select_item.dart | 43 +- .../sub_widgets/my_tokens_list.dart | 2 +- lib/pages/token_view/token_view.dart | 1169 +++++++++-------- lib/pages/wallet_view/wallet_view.dart | 2 - lib/route_generator.dart | 9 +- .../coins/ethereum/ethereum_wallet.dart | 12 +- .../tokens/ethereum/ethereum_token.dart | 66 +- lib/utilities/eth_commons.dart | 68 +- 8 files changed, 760 insertions(+), 611 deletions(-) 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 c7a6bf5c9..40fc7a154 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,7 +4,9 @@ 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'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -31,11 +33,12 @@ class MyTokenSelectItem extends ConsumerWidget { final ChangeNotifierProvider managerProvider; final String walletId; final String walletAddress; - final Map tokenData; + final Map tokenData; @override Widget build(BuildContext context, WidgetRef ref) { - int balance = int.parse(tokenData["balance"] as String); + print("TOKEN DATA IS $tokenData"); + int balance = tokenData["balance"] as int; int tokenDecimals = int.parse(tokenData["decimals"] as String); final balanceInDecimal = (balance / (pow(10, tokenDecimals))); @@ -51,38 +54,20 @@ class MyTokenSelectItem extends ConsumerWidget { BorderRadius.circular(Constants.size.circularBorderRadius), ), onPressed: () { - // ref - // .read(walletsChangeNotifierProvider) - // .getManagerProvider(walletId) + final mnemonicList = ref.read(managerProvider).mnemonic; - // final walletId = ref - // .read(managerProvider) - // .walletName; - // final manager = ref - // .read(walletsChangeNotifierProvider) - // .getManagerProvider(walletId) - - // arguments: Tuple2(walletId, managerProvider, walletAddress, - // tokenData["contractAddress"]) - - // arguments: Tuple2( - // walletId, - // ref - // .read(tokensChangeNotifierProvider) - // .getManagerProvider(walletId) - - // arguments: Tuple2( - // walletId, - // ref - // .read(tokensChangeNotifierProvider) - // .getManagerProvider(walletId) + final token = EthereumToken( + contractAddress: tokenData["contractAddress"] as String, + walletMnemonic: mnemonicList); Navigator.of(context).pushNamed( TokenView.routeName, - arguments: Tuple2( + arguments: Tuple3( walletId, - ref.read(tokensChangeNotifierProvider).getManagerProvider( - tokenData["contractAddress"] as String)), + ref + .read(walletsChangeNotifierProvider) + .getManagerProvider(walletId), + token), ); }, diff --git a/lib/pages/token_view/sub_widgets/my_tokens_list.dart b/lib/pages/token_view/sub_widgets/my_tokens_list.dart index a23614cc7..03731c89a 100644 --- a/lib/pages/token_view/sub_widgets/my_tokens_list.dart +++ b/lib/pages/token_view/sub_widgets/my_tokens_list.dart @@ -31,7 +31,7 @@ class MyTokensList extends StatelessWidget { managerProvider: managerProvider, walletId: walletId, walletAddress: walletAddress, - tokenData: tokens[index] as Map, + tokenData: tokens[index] as Map, ), ); }, diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index ee0d3e13d..10b478606 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -32,6 +32,7 @@ import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_ import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; +import 'package:stackwallet/services/tokens/ethereum/ethereum_token.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; @@ -49,37 +50,32 @@ import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:tuple/tuple.dart'; -import '../../services/tokens/token_manager.dart'; - /// [eventBus] should only be set during testing class TokenView extends ConsumerStatefulWidget { const TokenView({ Key? key, - required this.contractAddress, + required this.walletId, required this.managerProvider, - - // required this.walletAddress, - // required this.contractAddress, + required this.token, + this.eventBus, }) : super(key: key); static const String routeName = "/token"; static const double navBarHeight = 65.0; - final ChangeNotifierProvider managerProvider; - final String contractAddress; - // final String contractAddress; - // final String walletAddress; - - // final EventBus? eventBus; + final String walletId; + final ChangeNotifierProvider managerProvider; + final EthereumToken token; + final EventBus? eventBus; @override ConsumerState createState() => _TokenViewState(); } class _TokenViewState extends ConsumerState { - // late final EventBus eventBus; - late final String contractAddress; - late final ChangeNotifierProvider managerProvider; + late final EventBus eventBus; + late final String walletId; + late final ChangeNotifierProvider managerProvider; late final bool _shouldDisableAutoSyncOnLogOut; @@ -93,9 +89,10 @@ class _TokenViewState extends ConsumerState { @override void initState() { - contractAddress = widget.contractAddress; + walletId = widget.walletId; managerProvider = widget.managerProvider; + ref.read(managerProvider).isActiveWallet = true; if (!ref.read(managerProvider).shouldAutoSync) { // enable auto sync if it wasn't enabled when loading wallet ref.read(managerProvider).shouldAutoSync = true; @@ -119,44 +116,44 @@ class _TokenViewState extends ConsumerState { } } - // eventBus = - // widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance; + eventBus = + widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance; - // _syncStatusSubscription = - // eventBus.on().listen( - // (event) async { - // if (event.walletId == widget.walletId) { - // // switch (event.newStatus) { - // // case WalletSyncStatus.unableToSync: - // // break; - // // case WalletSyncStatus.synced: - // // break; - // // case WalletSyncStatus.syncing: - // // break; - // // } - // setState(() { - // _currentSyncStatus = event.newStatus; - // }); - // } - // }, - // ); + _syncStatusSubscription = + eventBus.on().listen( + (event) async { + if (event.walletId == widget.walletId) { + // switch (event.newStatus) { + // case WalletSyncStatus.unableToSync: + // break; + // case WalletSyncStatus.synced: + // break; + // case WalletSyncStatus.syncing: + // break; + // } + setState(() { + _currentSyncStatus = event.newStatus; + }); + } + }, + ); - // _nodeStatusSubscription = - // eventBus.on().listen( - // (event) async { - // if (event.walletId == widget.walletId) { - // // switch (event.newStatus) { - // // case NodeConnectionStatus.disconnected: - // // break; - // // case NodeConnectionStatus.connected: - // // break; - // // } - // setState(() { - // _currentNodeStatus = event.newStatus; - // }); - // } - // }, - // ); + _nodeStatusSubscription = + eventBus.on().listen( + (event) async { + if (event.walletId == widget.walletId) { + // switch (event.newStatus) { + // case NodeConnectionStatus.disconnected: + // break; + // case NodeConnectionStatus.connected: + // break; + // } + setState(() { + _currentNodeStatus = event.newStatus; + }); + } + }, + ); super.initState(); } @@ -183,6 +180,7 @@ class _TokenViewState extends ConsumerState { Navigator.of(context).popUntil( ModalRoute.withName(HomeView.routeName), ); + _logout(); return false; }, child: const StackDialog(title: "Tap back again to exit wallet"), @@ -197,19 +195,19 @@ class _TokenViewState extends ConsumerState { return false; } - // void _logout() async { - // if (_shouldDisableAutoSyncOnLogOut) { - // // disable auto sync if it was enabled only when loading wallet - // ref.read(managerProvider).shouldAutoSync = false; - // } - // ref.read(managerProvider.notifier).isActiveWallet = false; - // ref.read(transactionFilterProvider.state).state = null; - // if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled && - // ref.read(prefsChangeNotifierProvider).backupFrequencyType == - // BackupFrequencyType.afterClosingAWallet) { - // unawaited(ref.read(autoSWBServiceProvider).doBackup()); - // } - // } + void _logout() async { + if (_shouldDisableAutoSyncOnLogOut) { + // disable auto sync if it was enabled only when loading wallet + ref.read(managerProvider).shouldAutoSync = false; + } + ref.read(managerProvider.notifier).isActiveWallet = false; + ref.read(transactionFilterProvider.state).state = null; + if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled && + ref.read(prefsChangeNotifierProvider).backupFrequencyType == + BackupFrequencyType.afterClosingAWallet) { + unawaited(ref.read(autoSWBServiceProvider).doBackup()); + } + } Widget _buildNetworkIcon(WalletSyncStatus status) { switch (status) { @@ -237,115 +235,141 @@ class _TokenViewState extends ConsumerState { } } - // void _onExchangePressed(BuildContext context) async { - // unawaited(_cnLoadingService.loadAll(ref)); - // - // final coin = ref.read(managerProvider).coin; - // - // ref.read(currentExchangeNameStateProvider.state).state = - // ChangeNowExchange.exchangeName; - // // final contractAddress = ref.read(managerProvider).contractAddress; - // final contractAddress = "ref.read(managerProvider).contractAddress"; - // ref.read(prefsChangeNotifierProvider).exchangeRateType = - // 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()); - // - // if (currencies.isNotEmpty) { - // ref.read(exchangeFormStateProvider).setCurrencies( - // currencies.first, - // ref - // .read(availableChangeNowCurrenciesProvider) - // .currencies - // .firstWhere( - // (element) => - // element.ticker.toLowerCase() != - // coin.ticker.toLowerCase(), - // ), - // ); - // } - // - // - // } + void _onExchangePressed(BuildContext context) async { + unawaited(_cnLoadingService.loadAll(ref)); - // 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; - // } - // - // 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, - // ), - // ); - // } - // } 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", - // ), - // ); - // } - // } - // } + 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(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()); + + 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; + } + + 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, + ), + ); + } + } 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", + ), + ); + } + } + } void _loadCNData() { // unawaited future if (ref.read(prefsChangeNotifierProvider).externalCalls) { - _cnLoadingService.loadAll(ref, coin: Coin.ethereum); + _cnLoadingService.loadAll(ref, coin: ref.read(managerProvider).coin); } else { Logging.instance.log("User does not want to use external calls", level: LogLevel.Info); @@ -355,381 +379,440 @@ class _TokenViewState extends ConsumerState { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return const Scaffold(); - // final coin = ref.watch(managerProvider.select((value) => value.Co)); + final coin = ref.watch(managerProvider.select((value) => value.coin)); - // return WillPopScope( - // onWillPop: _onWillPop, - // child: Background( - // child: Scaffold( - // backgroundColor: - // Theme.of(context).extension()!.background, - // appBar: AppBar( - // leading: AppBarBackButton( - // onPressed: () { - // Navigator.of(context).pop(); - // }, - // ), - // titleSpacing: 0, - // title: Row( - // children: [ - // SvgPicture.asset( - // Assets.svg.iconFor(coin: Coin.ethereum), - // // color: Theme.of(context).extension()!.accentColorDark - // width: 24, - // height: 24, - // ), - // const SizedBox( - // width: 16, - // ), - // Expanded( - // child: Text( - // ref.watch( - // managerProvider.select((value) => "value.walletName")), - // 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( - // color: Theme.of(context).extension()!.background, - // child: Column( - // children: [ - // const SizedBox( - // height: 10, - // ), - // Center( - // child: Padding( - // padding: const EdgeInsets.symmetric(horizontal: 16), - // // child: WalletSummary( - // // walletId: walletId, - // // managerProvider: managerProvider, - // // initialSyncStatus: ref.watch(managerProvider - // // .select((value) => value.isRefreshing)) - // // ? WalletSyncStatus.syncing - // // : WalletSyncStatus.synced, - // // ), - // ), - // ), - // // if (coin == Coin.firo) - // // const SizedBox( - // // height: 10, - // // ), - // const SizedBox( - // height: 20, - // ), - // Padding( - // padding: const EdgeInsets.symmetric(horizontal: 16), - // child: Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Text( - // "Transactions", - // style: STextStyles.itemSubtitle(context).copyWith( - // color: Theme.of(context) - // .extension()! - // .textDark3, - // ), - // ), - // BlueTextButton( - // text: "See all", - // // onTap: () { - // // Navigator.of(context).pushNamed( - // // AllTransactionsView.routeName, - // // arguments: walletId, - // // ); - // // }, - // ), - // ], - // ), - // ), - // const SizedBox( - // height: 12, - // ), - // Expanded( - // child: Stack( - // children: [ - // Padding( - // padding: const EdgeInsets.symmetric(horizontal: 16), - // child: Padding( - // padding: const EdgeInsets.only(bottom: 14), - // child: ClipRRect( - // borderRadius: BorderRadius.vertical( - // top: Radius.circular( - // Constants.size.circularBorderRadius, - // ), - // bottom: Radius.circular( - // // TokenView.navBarHeight / 2.0, - // Constants.size.circularBorderRadius, - // ), - // ), - // child: Container( - // decoration: BoxDecoration( - // color: Colors.transparent, - // borderRadius: BorderRadius.circular( - // Constants.size.circularBorderRadius, - // ), - // ), - // child: Column( - // crossAxisAlignment: - // CrossAxisAlignment.stretch, - // children: [ - // // Expanded( - // // child: TransactionsList( - // // managerProvider: managerProvider, - // // walletId: walletId, - // // ), - // // ), - // ], - // ), - // ), - // ), - // ), - // ), - // Column( - // mainAxisAlignment: MainAxisAlignment.end, - // children: [ - // const Spacer(), - // 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; - // - // final walletName = ref - // .read(managerProvider) - // .walletName; - // - // List tokens = - // await getWalletTokens( - // walletAddress); - // - // await Navigator.of(context).pushNamed( - // MyTokensView.routeName, - // arguments: Tuple3(walletAddress, - // tokens, walletName), - // ); - // }, - // ), - // ), - // ), - // ], - // ), - // ], - // ) - // ], - // ), - // ), - // ], - // ), - // ), - // ), - // ), - // ), - // ); + return WillPopScope( + onWillPop: _onWillPop, + child: Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + _logout(); + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + // color: Theme.of(context).extension()!.accentColorDark + width: 24, + height: 24, + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Text( + ref.watch( + managerProvider.select((value) => value.walletName)), + 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( + color: Theme.of(context).extension()!.background, + child: Column( + children: [ + const SizedBox( + height: 10, + ), + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: WalletSummary( + walletId: walletId, + managerProvider: managerProvider, + initialSyncStatus: ref.watch(managerProvider + .select((value) => value.isRefreshing)) + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, + ), + ), + ), + 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, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transactions", + style: STextStyles.itemSubtitle(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ), + ), + BlueTextButton( + text: "See all", + onTap: () { + Navigator.of(context).pushNamed( + AllTransactionsView.routeName, + arguments: walletId, + ); + }, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + Expanded( + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Padding( + padding: const EdgeInsets.only(bottom: 14), + child: ClipRRect( + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + bottom: Radius.circular( + // TokenView.navBarHeight / 2.0, + Constants.size.circularBorderRadius, + ), + ), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Expanded( + child: TransactionsList( + managerProvider: managerProvider, + walletId: walletId, + ), + ), + ], + ), + ), + ), + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Spacer(), + 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), + ); + }, + ), + ), + ), + ], + ), + ], + ) + ], + ), + ), + ], + ), + ), + ), + ), + ), + ); } } diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index eef0fa755..03bc2941c 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -783,8 +783,6 @@ class _WalletViewState extends ConsumerState { .read(managerProvider) .currentReceivingAddress; - // String walletTokens = await - List tokens = await getWalletTokens(await ref .read(managerProvider) diff --git a/lib/route_generator.dart b/lib/route_generator.dart index e86dda8e3..04be917a3 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -117,6 +117,7 @@ import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/syncin 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'; @@ -1327,14 +1328,14 @@ class RouteGenerator { // } case TokenView.routeName: - if (args is Tuple2>) { + if (args + is Tuple3, EthereumToken>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => TokenView( - contractAddress: args.item1, + walletId: args.item1, managerProvider: args.item2, - // walletAddress: args.item3, - // contractAddress: args.item4, + token: args.item3, ), settings: RouteSettings( name: settings.name, diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 1a9e54a66..520186643 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -5,7 +5,6 @@ import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; import "package:hex/hex.dart"; -import 'package:bitcoindart/bitcoindart.dart'; import 'package:decimal/decimal.dart'; import 'package:devicelocale/devicelocale.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; @@ -22,7 +21,6 @@ import 'package:stackwallet/utilities/eth_commons.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/prefs.dart'; -import 'package:string_to_hex/string_to_hex.dart'; import 'package:web3dart/web3dart.dart'; import 'package:web3dart/web3dart.dart' as web3; import 'package:web3dart/web3dart.dart' as Transaction; @@ -45,7 +43,7 @@ import 'package:stackwallet/services/event_bus/events/global/updated_in_backgrou import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; -const int MINIMUM_CONFIRMATIONS = 5; +const int MINIMUM_CONFIRMATIONS = 3; //THis is used for mapping transactions per address from the block explorer class AddressTransaction { @@ -469,11 +467,15 @@ 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, @@ -505,8 +507,6 @@ class EthereumWallet extends CoinServiceAPI { String privateKey = getPrivateKey(mnemonic); _credentials = EthPrivateKey.fromHex(privateKey); - // _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); - // 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( @@ -1063,8 +1063,6 @@ class EthereumWallet extends CoinServiceAPI { @override set walletName(String newName) => _walletName = newName; - // Future - void stopNetworkAlivePinging() { _networkAliveTimer?.cancel(); _networkAliveTimer = null; diff --git a/lib/services/tokens/ethereum/ethereum_token.dart b/lib/services/tokens/ethereum/ethereum_token.dart index ef3bfc17f..5ddef7259 100644 --- a/lib/services/tokens/ethereum/ethereum_token.dart +++ b/lib/services/tokens/ethereum/ethereum_token.dart @@ -1,30 +1,69 @@ +import 'dart:convert'; + import 'package:decimal/decimal.dart'; +import 'package:http/http.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:web3dart/web3dart.dart'; + +class AbiRequestResponse { + final String message; + final String result; + final String status; + + const AbiRequestResponse({ + required this.message, + required this.result, + required this.status, + }); + + factory AbiRequestResponse.fromJson(Map json) { + return AbiRequestResponse( + message: json['message'] as String, + result: json['result'] as String, + status: json['status'] as String, + ); + } +} class EthereumToken extends TokenServiceAPI { @override late bool shouldAutoSync; - late String _walletId; late String _contractAddress; + late EthPrivateKey _credentials; + late Future> _walletMnemonic; late SecureStorageInterface _secureStore; + late String _tokenAbi; late final TransactionNotificationTracker txTracker; + String rpcUrl = + 'https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba'; + EthereumToken({ required String contractAddress, - required String walletId, - required SecureStorageInterface secureStore, - required TransactionNotificationTracker tracker, + required Future> walletMnemonic, + // required SecureStorageInterface secureStore, }) { - txTracker = tracker; - _walletId = walletId; _contractAddress = contractAddress; - _secureStore = secureStore; + _walletMnemonic = walletMnemonic; + // _secureStore = secureStore; + } + + Future fetchTokenAbi() async { + final response = await get(Uri.parse( + "https://api.etherscan.io/api?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'); + } } @override @@ -64,7 +103,18 @@ class EthereumToken extends TokenServiceAPI { Future get fees => throw UnimplementedError(); @override - Future initializeExisting() { + Future initializeExisting() async { + 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(); } diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart index a8286993b..c3c95f8a6 100644 --- a/lib/utilities/eth_commons.dart +++ b/lib/utilities/eth_commons.dart @@ -1,6 +1,11 @@ import 'dart:convert'; import 'package:http/http.dart'; +import 'package:ethereum_addresses/ethereum_addresses.dart'; +import 'flutter_secure_storage_interface.dart'; +import 'package:bip32/bip32.dart' as bip32; +import 'package:bip39/bip39.dart' as bip39; +import "package:hex/hex.dart"; class AccountModule { final String message; @@ -22,11 +27,13 @@ class AccountModule { } } -const _blockExplorer = "https://blockscout.com/eth/mainnet/api?"; +const _blockExplorer = "https://api.etherscan.io/api?"; +late SecureStorageInterface _secureStore; +const _hdPath = "m/44'/60'/0'/0"; Future fetchAccountModule(String action, String address) async { final response = await get(Uri.parse( - "${_blockExplorer}module=account&action=$action&address=$address")); + "${_blockExplorer}module=account&action=$action&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); if (response.statusCode == 200) { return AccountModule.fromJson( json.decode(response.body) as Map); @@ -36,21 +43,48 @@ Future fetchAccountModule(String action, String address) async { } Future> getWalletTokens(String address) async { - AccountModule tokens = await fetchAccountModule("tokenlist", address); - //THIS IS ONLY HARD CODED UNTIL API WORKS AGAIN - TODO REMOVE HARDCODED - return [ - { - "balance": "369039500000000000", - "contractAddress": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", - "decimals": "18", - "name": "Uniswap", - "symbol": "UNI", - "type": "ERC-20" - } - ]; - + AccountModule tokens = await fetchAccountModule("tokentx", address); + List tokensList = []; + var tokenMap = {}; if (tokens.message == "OK") { - return tokens.result as List; + final allTxs = tokens.result; + print("RESULT IS $allTxs"); + allTxs.forEach((element) { + String key = element["tokenSymbol"] as String; + tokenMap[key] = {}; + tokenMap[key]["balance"] = 0; + + if (tokenMap.containsKey(key)) { + tokenMap[key]["contractAddress"] = element["contractAddress"] as String; + tokenMap[key]["decimals"] = element["tokenDecimal"]; + tokenMap[key]["name"] = element["tokenName"]; + tokenMap[key]["symbol"] = element["tokenSymbol"]; + if (checksumEthereumAddress(address) == address) { + tokenMap[key]["balance"] += int.parse(element["value"] as String); + } else { + tokenMap[key]["balance"] -= int.parse(element["value"] as String); + } + } + }); + + tokenMap.forEach((key, value) { + tokensList.add(value as Map); + }); + return tokensList; } - return []; + return []; +} + +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); }