diff --git a/lib/pages/token_view/my_tokens_view.dart b/lib/pages/token_view/my_tokens_view.dart index f5a7f8bcd..f6e20d6fe 100644 --- a/lib/pages/token_view/my_tokens_view.dart +++ b/lib/pages/token_view/my_tokens_view.dart @@ -26,18 +26,22 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart'; import 'package:stackwallet/utilities/eth_commons.dart'; +import 'package:stackwallet/services/coins/manager.dart'; + class MyTokensView extends ConsumerStatefulWidget { const MyTokensView({ Key? key, + required this.managerProvider, + required this.walletId, required this.walletAddress, required this.tokens, - required this.walletName, }) : super(key: key); static const String routeName = "/myTokens"; + final ChangeNotifierProvider managerProvider; + final String walletId; final String walletAddress; final List tokens; - final String walletName; @override ConsumerState createState() => _TokenDetailsViewState(); @@ -45,12 +49,13 @@ class MyTokensView extends ConsumerStatefulWidget { class _TokenDetailsViewState extends ConsumerState { late final String walletAddress; + // late final String walletName; late final TextEditingController _searchController; final searchFieldFocusNode = FocusNode(); @override void initState() { - walletAddress = widget.walletAddress; + // walletAddress = widget.walletAddress; _searchController = TextEditingController(); super.initState(); @@ -101,7 +106,7 @@ class _TokenDetailsViewState extends ConsumerState { width: 12, ), Text( - "${widget.walletName} Tokens", + "${ref.read(widget.managerProvider).walletName} Tokens", style: STextStyles.desktopH3(context), ), ], @@ -123,7 +128,7 @@ class _TokenDetailsViewState extends ConsumerState { }, ), title: Text( - "${widget.walletName} Tokens", + "${ref.read(widget.managerProvider).walletName} Tokens", style: STextStyles.navBarTitle(context), ), actions: [ @@ -261,7 +266,10 @@ class _TokenDetailsViewState extends ConsumerState { ), Expanded( child: MyTokensList( - tokens: widget.tokens, walletAddress: walletAddress), + managerProvider: widget.managerProvider, + walletId: widget.walletId, + tokens: widget.tokens, + walletAddress: widget.walletAddress), ), ], ), 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 8d62aaeef..c7a6bf5c9 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,6 +4,7 @@ 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/providers/global/tokens_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -13,14 +14,22 @@ import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:tuple/tuple.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; + +import 'package:stackwallet/services/coins/manager.dart'; + class MyTokenSelectItem extends ConsumerWidget { const MyTokenSelectItem( {Key? key, + required this.managerProvider, + required this.walletId, required this.walletAddress, required this.tokenData, required}) : super(key: key); + final ChangeNotifierProvider managerProvider; + final String walletId; final String walletAddress; final Map tokenData; @@ -42,9 +51,38 @@ class MyTokenSelectItem extends ConsumerWidget { BorderRadius.circular(Constants.size.circularBorderRadius), ), onPressed: () { + // ref + // .read(walletsChangeNotifierProvider) + // .getManagerProvider(walletId) + + // 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) + Navigator.of(context).pushNamed( TokenView.routeName, - arguments: Tuple2(walletAddress, tokenData["contractAddress"]), + arguments: Tuple2( + walletId, + ref.read(tokensChangeNotifierProvider).getManagerProvider( + tokenData["contractAddress"] as String)), ); }, 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 f9c5b31b9..a23614cc7 100644 --- a/lib/pages/token_view/sub_widgets/my_tokens_list.dart +++ b/lib/pages/token_view/sub_widgets/my_tokens_list.dart @@ -1,14 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/my_token_select_item.dart'; +import 'package:stackwallet/services/coins/manager.dart'; class MyTokensList extends StatelessWidget { const MyTokensList({ Key? key, + required this.managerProvider, + required this.walletId, required this.tokens, required this.walletAddress, }) : super(key: key); + final ChangeNotifierProvider managerProvider; + final String walletId; final List tokens; final String walletAddress; @@ -23,6 +28,8 @@ class MyTokensList extends StatelessWidget { return Padding( padding: const EdgeInsets.all(4), child: MyTokenSelectItem( + managerProvider: managerProvider, + walletId: walletId, walletAddress: walletAddress, tokenData: tokens[index] as Map, ), diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 046a4fb4a..ee0d3e13d 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -49,35 +49,37 @@ 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.walletId, required this.contractAddress, required this.managerProvider, - this.eventBus, - required this.walletAddress, + + // required this.walletAddress, + // required this.contractAddress, }) : super(key: key); static const String routeName = "/token"; static const double navBarHeight = 65.0; - final String walletId; + final ChangeNotifierProvider managerProvider; final String contractAddress; - final String walletAddress; - final ChangeNotifierProvider managerProvider; + // final String contractAddress; + // final String walletAddress; - final EventBus? eventBus; + // final EventBus? eventBus; @override ConsumerState createState() => _TokenViewState(); } class _TokenViewState extends ConsumerState { - late final EventBus eventBus; - late final String walletId; - late final ChangeNotifierProvider managerProvider; + // late final EventBus eventBus; + late final String contractAddress; + late final ChangeNotifierProvider managerProvider; late final bool _shouldDisableAutoSyncOnLogOut; @@ -91,10 +93,9 @@ class _TokenViewState extends ConsumerState { @override void initState() { - walletId = widget.walletId; + contractAddress = widget.contractAddress; 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; @@ -118,44 +119,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(); } @@ -182,7 +183,6 @@ 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 +197,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,141 +237,115 @@ class _TokenViewState extends ConsumerState { } } - void _onExchangePressed(BuildContext context) async { - unawaited(_cnLoadingService.loadAll(ref)); + // 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(), + // ), + // ); + // } + // + // + // } - 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", - ), - ); - } - } - } + // 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: ref.read(managerProvider).coin); + _cnLoadingService.loadAll(ref, coin: Coin.ethereum); } else { Logging.instance.log("User does not want to use external calls", level: LogLevel.Info); @@ -381,443 +355,381 @@ class _TokenViewState extends ConsumerState { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); + return const Scaffold(); - final coin = ref.watch(managerProvider.select((value) => value.coin)); + // final coin = ref.watch(managerProvider.select((value) => value.Co)); - 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; - - 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: () { + // 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), + // ); + // }, + // ), + // ), + // ), + // ], + // ), + // ], + // ) + // ], + // ), + // ), + // ], + // ), + // ), + // ), + // ), + // ), + // ); } } diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index b70570b05..eef0fa755 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -783,18 +783,17 @@ class _WalletViewState extends ConsumerState { .read(managerProvider) .currentReceivingAddress; - final walletName = ref - .read(managerProvider) - .walletName; + // String walletTokens = await List tokens = - await getWalletTokens( - walletAddress); + await getWalletTokens(await ref + .read(managerProvider) + .currentReceivingAddress); await Navigator.of(context).pushNamed( MyTokensView.routeName, - arguments: Tuple3(walletAddress, - tokens, walletName), + arguments: Tuple4(managerProvider, + walletId, walletAddress, tokens), ); }, ), diff --git a/lib/providers/global/tokens_provider.dart b/lib/providers/global/tokens_provider.dart new file mode 100644 index 000000000..388c45971 --- /dev/null +++ b/lib/providers/global/tokens_provider.dart @@ -0,0 +1,23 @@ +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 new file mode 100644 index 000000000..053bc035e --- /dev/null +++ b/lib/providers/global/tokens_service_provider.dart @@ -0,0 +1,20 @@ +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 0f076eade..e86dda8e3 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/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'; @@ -1295,13 +1296,15 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case MyTokensView.routeName: - if (args is Tuple3, String>) { + if (args is Tuple4, String, String, + List>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => MyTokensView( - walletAddress: args.item1, - tokens: args.item2, - walletName: args.item3), + managerProvider: args.item1, + walletId: args.item2, + walletAddress: args.item3, + tokens: args.item4), settings: RouteSettings( name: settings.name, ), @@ -1309,13 +1312,29 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + // case WalletView.routeName: + // if (args is Tuple2>) { + // return getRoute( + // shouldUseMaterialRoute: useMaterialPageRoute, + // builder: (_) => WalletView( + // walletId: args.item1, + // managerProvider: args.item2, + // ), + // settings: RouteSettings( + // name: settings.name, + // ), + // ); + // } + case TokenView.routeName: - if (args is Tuple2) { + if (args is Tuple2>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => TokenView( - walletAddress: args.item1, - contractAddress: args.item2, + contractAddress: args.item1, + managerProvider: args.item2, + // walletAddress: args.item3, + // contractAddress: args.item4, ), settings: RouteSettings( name: settings.name, diff --git a/lib/services/coins/erc_20/erc_wallet.dart b/lib/services/coins/erc_20/erc_wallet.dart deleted file mode 100644 index e36e2c46b..000000000 --- a/lib/services/coins/erc_20/erc_wallet.dart +++ /dev/null @@ -1,1007 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:math'; -import 'package:bip39/bip39.dart' as bip39; -import 'package:decimal/decimal.dart'; -import 'package:devicelocale/devicelocale.dart'; -import 'package:ethereum_addresses/ethereum_addresses.dart'; -import 'package:stackwallet/models/node_model.dart'; -import 'package:stackwallet/models/paymint/fee_object_model.dart'; -import 'package:stackwallet/models/paymint/transactions_model.dart'; -import 'package:stackwallet/models/paymint/utxo_model.dart'; -import 'package:stackwallet/services/price.dart'; -import 'package:stackwallet/services/transaction_notification_tracker.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; -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; -import 'package:stackwallet/models/models.dart' as models; - -import 'package:http/http.dart'; - -import 'package:stackwallet/hive/db.dart'; -import 'package:stackwallet/utilities/logger.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/refresh_percent_changed_event.dart'; -import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; -import 'package:stackwallet/services/event_bus/global_event_bus.dart'; - -import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/services/notifications_api.dart'; -import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; -import 'package:stackwallet/services/node_service.dart'; -import 'package:stackwallet/utilities/default_nodes.dart'; - -const int MINIMUM_CONFIRMATIONS = 5; - -//THis is used for mapping transactions per address from the block explorer -class AddressTransaction { - final String message; - final List result; - final String status; - - const AddressTransaction({ - required this.message, - required this.result, - required this.status, - }); - - factory AddressTransaction.fromJson(Map json) { - return AddressTransaction( - message: json['message'] as String, - result: json['result'] as List, - status: json['status'] as String, - ); - } -} - -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 TokenWallet extends CoinServiceAPI { - NodeModel? _ethNode; - final _gasLimit = 21000; - final _blockExplorer = "https://blockscout.com/eth/mainnet/api?"; - final _gasTrackerUrl = "https://beaconcha.in/api/v1/execution/gasnow"; - - @override - set isFavorite(bool markFavorite) {} - - @override - bool get isFavorite { - throw UnimplementedError(); - } - - @override - Coin get coin => Coin.ethereum; - - late SecureStorageInterface _secureStore; - late final TransactionNotificationTracker txTracker; - late PriceAPI _priceAPI; - final _prefs = Prefs.instance; - bool longMutex = false; - - //TODO - move shared logic to eth_commons - Future getCurrentNode() async { - return NodeService(secureStorageInterface: _secureStore) - .getPrimaryNodeFor(coin: coin) ?? - DefaultNodes.getNodeFor(coin); - } - - Future getEthClient() async { - final node = await getCurrentNode(); - return Web3Client(node.host, Client()); - } - - late EthPrivateKey _credentials; - - TokenWallet({ - required String walletId, - required String walletName, - required Coin coin, - PriceAPI? priceAPI, - required SecureStorageInterface secureStore, - required TransactionNotificationTracker tracker, - }) { - txTracker = tracker; - _walletId = walletId; - _walletName = walletName; - // _coin = coin; - - _priceAPI = priceAPI ?? PriceAPI(Client()); - _secureStore = secureStore; - } - - bool _shouldAutoSync = false; - - @override - bool get shouldAutoSync => _shouldAutoSync; - - @override - set shouldAutoSync(bool shouldAutoSync) { - if (_shouldAutoSync != shouldAutoSync) { - _shouldAutoSync = shouldAutoSync; - if (!shouldAutoSync) { - timer?.cancel(); - timer = null; - stopNetworkAlivePinging(); - } else { - startNetworkAlivePinging(); - refresh(); - } - } - } - - @override - String get walletName => _walletName; - late String _walletName; - - Timer? timer; - Timer? _networkAliveTimer; - - @override - Future> get allOwnAddresses => - _allOwnAddresses ??= _fetchAllOwnAddresses(); - Future>? _allOwnAddresses; - - Future> _fetchAllOwnAddresses() async { - List addresses = []; - final ownAddress = _credentials.address; - addresses.add(ownAddress.toString()); - return addresses; - } - - @override - Future get availableBalance async { - Web3Client client = await getEthClient(); - EtherAmount ethBalance = await client.getBalance(_credentials.address); - return Decimal.parse(ethBalance.getValueInUnit(EtherUnit.ether).toString()); - } - - @override - Future get balanceMinusMaxFee async => - (await availableBalance) - - (Decimal.fromInt((await maxFee)) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal(); - - @override - Future confirmSend({required Map txData}) async { - Web3Client client = await getEthClient(); - final int chainId = await client.getNetworkId(); - final amount = txData['recipientAmt']; - final decimalAmount = - Format.satoshisToAmount(amount as int, coin: Coin.ethereum); - final bigIntAmount = amountToBigInt(decimalAmount.toDouble()); - - final tx = Transaction.Transaction( - to: EthereumAddress.fromHex(txData['address'] as String), - gasPrice: - EtherAmount.fromUnitAndValue(EtherUnit.wei, txData['feeInWei']), - maxGas: _gasLimit, - value: EtherAmount.inWei(bigIntAmount)); - final transaction = - await client.sendTransaction(_credentials, tx, chainId: chainId); - - 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; - final checkSumAddress = - checksumEthereumAddress(_currentReceivingAddress.toString()); - return checkSumAddress; - } - - @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); - } - - @override - Future exit() async { - _hasCalledExit = true; - timer?.cancel(); - timer = null; - stopNetworkAlivePinging(); - } - - @override - Future get fees => _feeObject ??= _getFees(); - 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'); - } - } - - //Full rescan is not needed for ETH since we have a balance - @override - Future fullRescan( - int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) { - // TODO: implement fullRescan - throw UnimplementedError(); - } - - @override - Future generateNewAddress() { - // TODO: implement generateNewAddress - might not be needed for ETH - throw UnimplementedError(); - } - - bool _hasCalledExit = false; - - @override - bool get hasCalledExit => _hasCalledExit; - - @override - Future initializeExisting() async { - //First get mnemonic so we can initialize credentials - final mnemonicString = - await _secureStore.read(key: '${_walletId}_mnemonic'); - - _credentials = - EthPrivateKey.fromHex(StringToHex.toHexString(mnemonicString)); - - Logging.instance.log("Opening existing ${coin.prettyName} wallet.", - level: LogLevel.Info); - - if ((DB.instance.get(boxName: walletId, key: "id")) == null) { - throw Exception( - "Attempted to initialize an existing wallet using an unknown wallet ID!"); - } - await _prefs.init(); - final data = - DB.instance.get(boxName: walletId, key: "latest_tx_model") - as TransactionData?; - if (data != null) { - _transactionData = Future(() => data); - } - } - - @override - Future initializeNew() async { - await _prefs.init(); - final String mnemonic = bip39.generateMnemonic(strength: 256); - _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); - await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic); - - //Store credentials in secure store - await _secureStore.write( - key: '${_walletId}_credentials', value: _credentials.toString()); - - await DB.instance - .put(boxName: walletId, key: "id", value: _walletId); - await DB.instance.put( - boxName: walletId, key: 'receivingAddresses', value: ["0"]); - await DB.instance - .put(boxName: walletId, key: "receivingIndex", value: 0); - await DB.instance - .put(boxName: walletId, key: "changeIndex", value: 0); - await DB.instance.put( - boxName: walletId, - key: 'blocked_tx_hashes', - value: ["0xdefault"], - ); // A list of transaction hashes to represent frozen utxos in wallet - // initialize address book entries - await DB.instance.put( - boxName: walletId, - key: 'addressBookEntries', - value: {}); - await DB.instance - .put(boxName: walletId, key: "isFavorite", value: false); - } - - bool _isConnected = false; - - @override - bool get isConnected => _isConnected; - - @override - bool get isRefreshing => refreshMutex; - - bool refreshMutex = false; - - @override - Future get maxFee async { - final fee = (await fees).fast; - final feeEstimate = await estimateFeeFor(0, fee); - return feeEstimate; - } - - @override - Future> get mnemonic => _getMnemonicList(); - - Future get chainHeight async { - Web3Client client = await getEthClient(); - try { - final result = await client.getBlockNumber(); - - return result; - } catch (e, s) { - Logging.instance.log("Exception caught in chainHeight: $e\n$s", - level: LogLevel.Error); - return -1; - } - } - - int get storedChainHeight { - final storedHeight = DB.instance - .get(boxName: walletId, key: "storedChainHeight") as int?; - return storedHeight ?? 0; - } - - Future updateStoredChainHeight({required int newHeight}) async { - await DB.instance.put( - boxName: walletId, key: "storedChainHeight", value: newHeight); - } - - Future> _getMnemonicList() async { - final mnemonicString = - await _secureStore.read(key: '${_walletId}_mnemonic'); - if (mnemonicString == null) { - return []; - } - final List data = mnemonicString.split(' '); - return data; - } - - @override - // TODO: implement pendingBalance - Not needed since we don't use UTXOs to get a balance - Future get pendingBalance => throw UnimplementedError(); - - @override - Future> prepareSend( - {required String address, - required int satoshiAmount, - 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, - }; - - return txData; - } - - @override - Future recoverFromMnemonic( - {required String mnemonic, - required int maxUnusedAddressGap, - required int maxNumberOfIndexesToCheck, - required int height}) async { - longMutex = true; - final start = DateTime.now(); - - try { - if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) { - longMutex = false; - throw Exception("Attempted to overwrite mnemonic on restore!"); - } - - await _secureStore.write( - key: '${_walletId}_mnemonic', value: mnemonic.trim()); - - _credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic)); - - await DB.instance - .put(boxName: walletId, key: "id", value: _walletId); - await DB.instance - .put(boxName: walletId, key: "isFavorite", value: false); - } catch (e, s) { - Logging.instance.log( - "Exception rethrown from recoverFromMnemonic(): $e\n$s", - level: LogLevel.Error); - longMutex = false; - rethrow; - } - - longMutex = false; - final end = DateTime.now(); - Logging.instance.log( - "$walletName recovery time: ${end.difference(start).inMilliseconds} millis", - level: LogLevel.Info); - } - - Future refreshIfThereIsNewData() async { - Web3Client client = await getEthClient(); - if (longMutex) return false; - if (_hasCalledExit) return false; - final currentChainHeight = await chainHeight; - - try { - bool needsRefresh = false; - Set txnsToCheck = {}; - - for (final String txid in txTracker.pendings) { - if (!txTracker.wasNotifiedConfirmed(txid)) { - txnsToCheck.add(txid); - } - } - - for (String txid in txnsToCheck) { - final txn = await client.getTransactionByHash(txid); - final int txBlockNumber = txn.blockNumber.blockNum; - - final int txConfirmations = currentChainHeight - txBlockNumber; - bool isUnconfirmed = txConfirmations < MINIMUM_CONFIRMATIONS; - if (!isUnconfirmed) { - needsRefresh = true; - break; - } - } - if (!needsRefresh) { - var allOwnAddresses = await _fetchAllOwnAddresses(); - AddressTransaction addressTransactions = - await fetchAddressTransactions(allOwnAddresses.elementAt(0)); - final txData = await transactionData; - if (addressTransactions.message == "OK") { - final allTxs = addressTransactions.result; - allTxs.forEach((element) { - if (txData.findTransaction(element["hash"] as String) == null) { - Logging.instance.log( - " txid not found in address history already ${element["hash"]}", - level: LogLevel.Info); - needsRefresh = true; - } - }); - } - } - return needsRefresh; - } catch (e, s) { - Logging.instance.log( - "Exception caught in refreshIfThereIsNewData: $e\n$s", - level: LogLevel.Error); - rethrow; - } - } - - Future getAllTxsToWatch( - TransactionData txData, - ) async { - if (_hasCalledExit) return; - List unconfirmedTxnsToNotifyPending = []; - List unconfirmedTxnsToNotifyConfirmed = []; - - for (final chunk in txData.txChunks) { - for (final tx in chunk.transactions) { - if (tx.confirmedStatus) { - // get all transactions that were notified as pending but not as confirmed - if (txTracker.wasNotifiedPending(tx.txid) && - !txTracker.wasNotifiedConfirmed(tx.txid)) { - unconfirmedTxnsToNotifyConfirmed.add(tx); - } - } else { - // get all transactions that were not notified as pending yet - if (!txTracker.wasNotifiedPending(tx.txid)) { - unconfirmedTxnsToNotifyPending.add(tx); - } - } - } - } - - // notify on unconfirmed transactions - for (final tx in unconfirmedTxnsToNotifyPending) { - if (tx.txType == "Received") { - unawaited(NotificationApi.showNotification( - title: "Incoming transaction", - body: walletName, - walletId: walletId, - iconAssetName: Assets.svg.iconFor(coin: coin), - date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), - shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, - coinName: coin.name, - txid: tx.txid, - confirmations: tx.confirmations, - requiredConfirmations: MINIMUM_CONFIRMATIONS, - )); - await txTracker.addNotifiedPending(tx.txid); - } else if (tx.txType == "Sent") { - unawaited(NotificationApi.showNotification( - title: "Sending transaction", - body: walletName, - walletId: walletId, - iconAssetName: Assets.svg.iconFor(coin: coin), - date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), - shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, - coinName: coin.name, - txid: tx.txid, - confirmations: tx.confirmations, - requiredConfirmations: MINIMUM_CONFIRMATIONS, - )); - await txTracker.addNotifiedPending(tx.txid); - } - } - - // notify on confirmed - for (final tx in unconfirmedTxnsToNotifyConfirmed) { - if (tx.txType == "Received") { - unawaited(NotificationApi.showNotification( - title: "Incoming transaction confirmed", - body: walletName, - walletId: walletId, - iconAssetName: Assets.svg.iconFor(coin: coin), - date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), - shouldWatchForUpdates: false, - coinName: coin.name, - )); - await txTracker.addNotifiedConfirmed(tx.txid); - } else if (tx.txType == "Sent") { - unawaited(NotificationApi.showNotification( - title: "Outgoing transaction confirmed", - body: walletName, - walletId: walletId, - iconAssetName: Assets.svg.iconFor(coin: coin), - date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), - shouldWatchForUpdates: false, - coinName: coin.name, - )); - await txTracker.addNotifiedConfirmed(tx.txid); - } - } - } - - @override - Future refresh() async { - if (refreshMutex) { - Logging.instance.log("$walletId $walletName refreshMutex denied", - level: LogLevel.Info); - return; - } else { - refreshMutex = true; - } - - try { - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - coin, - ), - ); - - GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId)); - GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); - - final currentHeight = await chainHeight; - const storedHeight = 1; //await storedChainHeight; - - Logging.instance - .log("chain height: $currentHeight", level: LogLevel.Info); - Logging.instance - .log("cached height: $storedHeight", level: LogLevel.Info); - - if (currentHeight != storedHeight) { - if (currentHeight != -1) { - // -1 failed to fetch current height - unawaited(updateStoredChainHeight(newHeight: currentHeight)); - } - - GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); - - final newTxData = _fetchTransactionData(); - GlobalEventBus.instance - .fire(RefreshPercentChangedEvent(0.50, walletId)); - - final feeObj = _getFees(); - GlobalEventBus.instance - .fire(RefreshPercentChangedEvent(0.60, walletId)); - - _transactionData = Future(() => newTxData); - - GlobalEventBus.instance - .fire(RefreshPercentChangedEvent(0.70, walletId)); - _feeObject = Future(() => feeObj); - GlobalEventBus.instance - .fire(RefreshPercentChangedEvent(0.80, walletId)); - - final allTxsToWatch = getAllTxsToWatch(await newTxData); - await Future.wait([ - newTxData, - feeObj, - allTxsToWatch, - ]); - GlobalEventBus.instance - .fire(RefreshPercentChangedEvent(0.90, walletId)); - } - refreshMutex = false; - GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId)); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - coin, - ), - ); - - if (shouldAutoSync) { - timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { - Logging.instance.log( - "Periodic refresh check for $walletId $walletName in object instance: $hashCode", - level: LogLevel.Info); - if (await refreshIfThereIsNewData()) { - await refresh(); - GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( - "New data found in $walletId $walletName in background!", - walletId)); - } - }); - } - } catch (error, strace) { - refreshMutex = false; - GlobalEventBus.instance.fire( - NodeConnectionStatusChangedEvent( - NodeConnectionStatus.disconnected, - walletId, - coin, - ), - ); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.unableToSync, - walletId, - coin, - ), - ); - Logging.instance.log( - "Caught exception in refreshWalletData(): $error\n$strace", - level: LogLevel.Warning); - } - } - - @override - Future send( - {required String toAddress, - required int amount, - Map args = const {}}) { - // TODO: implement send - throw UnimplementedError(); - } - - @override - Future testNetworkConnection() async { - Web3Client client = await getEthClient(); - try { - final result = await client.isListeningForNetwork(); - return result; - } catch (_) { - return false; - } - } - - void _periodicPingCheck() async { - bool hasNetwork = await testNetworkConnection(); - _isConnected = hasNetwork; - if (_isConnected != hasNetwork) { - NodeConnectionStatus status = hasNetwork - ? NodeConnectionStatus.connected - : NodeConnectionStatus.disconnected; - GlobalEventBus.instance - .fire(NodeConnectionStatusChangedEvent(status, walletId, coin)); - } - } - - @override - Future get totalBalance async { - Web3Client client = await getEthClient(); - EtherAmount ethBalance = await client.getBalance(_credentials.address); - return Decimal.parse(ethBalance.getValueInUnit(EtherUnit.ether).toString()); - } - - @override - Future get transactionData => - _transactionData ??= _fetchTransactionData(); - Future? _transactionData; - - TransactionData? cachedTxData; - - @override - // TODO: implement unspentOutputs - NOT NEEDED, ETH DOES NOT USE UTXOs - Future> get unspentOutputs => throw UnimplementedError(); - - @override - Future updateNode(bool shouldRefresh) async { - _ethNode = NodeService(secureStorageInterface: _secureStore) - .getPrimaryNodeFor(coin: coin) ?? - DefaultNodes.getNodeFor(coin); - - if (shouldRefresh) { - unawaited(refresh()); - } - } - - @override - Future updateSentCachedTxData(Map txData) async { - final priceData = - await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); - Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; - 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) { - return isValidEthereumAddress(address); - } - - Future fetchAddressTransactions(String address) async { - final response = await get(Uri.parse( - "${_blockExplorer}module=account&action=txlist&address=$address")); - - if (response.statusCode == 200) { - return AddressTransaction.fromJson( - json.decode(response.body) as Map); - } else { - throw Exception('Failed to load transactions'); - } - } - - Future _fetchTransactionData() async { - String thisAddress = await currentReceivingAddress; - final cachedTransactions = - DB.instance.get(boxName: walletId, key: 'latest_tx_model') - as TransactionData?; - int latestTxnBlockHeight = - DB.instance.get(boxName: walletId, key: "storedTxnDataHeight") - as int? ?? - 0; - - final priceData = - await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); - Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; - final List> midSortedArray = []; - - AddressTransaction txs = await fetchAddressTransactions(thisAddress); - - if (txs.message == "OK") { - final allTxs = txs.result; - allTxs.forEach((element) { - Map midSortedTx = {}; - // create final tx map - midSortedTx["txid"] = element["hash"]; - int confirmations = int.parse(element['confirmations'].toString()); - - int transactionAmount = int.parse(element['value'].toString()); - const decimal = 18; //Eth has up to 18 decimal places - final transactionAmountInDecimal = - transactionAmount / (pow(10, decimal)); - - //Convert to satoshi, default display for other coins - final satAmount = Format.decimalAmountToSatoshis( - Decimal.parse(transactionAmountInDecimal.toString()), coin); - - midSortedTx["confirmed_status"] = - (confirmations != 0) && (confirmations >= MINIMUM_CONFIRMATIONS); - midSortedTx["confirmations"] = confirmations; - midSortedTx["timestamp"] = element["timeStamp"]; - - if (checksumEthereumAddress(element["from"].toString()) == - thisAddress) { - midSortedTx["txType"] = "Sent"; - } else { - midSortedTx["txType"] = "Received"; - } - - midSortedTx["amount"] = satAmount; - final String worthNow = ((currentPrice * Decimal.fromInt(satAmount)) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal(scaleOnInfinitePrecision: 2) - .toStringAsFixed(2); - - //Calculate fees (GasLimit * gasPrice) - int txFee = int.parse(element['gasPrice'].toString()) * - int.parse(element['gasUsed'].toString()); - final txFeeDecimal = txFee / (pow(10, decimal)); - - midSortedTx["worthNow"] = worthNow; - midSortedTx["worthAtBlockTimestamp"] = worthNow; - midSortedTx["aliens"] = []; - midSortedTx["fees"] = Format.decimalAmountToSatoshis( - Decimal.parse(txFeeDecimal.toString()), coin); - midSortedTx["address"] = element["to"]; - midSortedTx["inputSize"] = 1; - midSortedTx["outputSize"] = 1; - midSortedTx["inputs"] = []; - midSortedTx["outputs"] = []; - midSortedTx["height"] = int.parse(element['blockNumber'].toString()); - - midSortedArray.add(midSortedTx); - }); - } - - midSortedArray.sort((a, b) => - (int.parse(b['timestamp'].toString())) - - (int.parse(a['timestamp'].toString()))); - - // buildDateTimeChunks - final Map result = {"dateTimeChunks": []}; - final dateArray = []; - - for (int i = 0; i < midSortedArray.length; i++) { - final txObject = midSortedArray[i]; - final date = - extractDateFromTimestamp(int.parse(txObject['timestamp'].toString())); - final txTimeArray = [txObject["timestamp"], date]; - - if (dateArray.contains(txTimeArray[1])) { - result["dateTimeChunks"].forEach((dynamic chunk) { - if (extractDateFromTimestamp( - int.parse(chunk['timestamp'].toString())) == - txTimeArray[1]) { - if (chunk["transactions"] == null) { - chunk["transactions"] = >[]; - } - chunk["transactions"].add(txObject); - } - }); - } else { - dateArray.add(txTimeArray[1]); - final chunk = { - "timestamp": txTimeArray[0], - "transactions": [txObject], - }; - result["dateTimeChunks"].add(chunk); - } - } - - final transactionsMap = cachedTransactions?.getAllTransactions() ?? {}; - transactionsMap - .addAll(TransactionData.fromJson(result).getAllTransactions()); - - final txModel = TransactionData.fromMap(transactionsMap); - - await DB.instance.put( - boxName: walletId, - key: 'storedTxnDataHeight', - value: latestTxnBlockHeight); - await DB.instance.put( - boxName: walletId, key: 'latest_tx_model', value: txModel); - - cachedTxData = txModel; - return txModel; - } - - @override - String get walletId => _walletId; - late String _walletId; - - @override - set walletName(String newName) => _walletName = newName; - - void stopNetworkAlivePinging() { - _networkAliveTimer?.cancel(); - _networkAliveTimer = null; - } - - void startNetworkAlivePinging() { - // call once on start right away - _periodicPingCheck(); - - // then periodically check - _networkAliveTimer = Timer.periodic( - Constants.networkAliveTimerDuration, - (_) async { - _periodicPingCheck(); - }, - ); - } -} diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 0bf1a6373..ce75fea22 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -84,9 +84,19 @@ class GasTracker { class EthereumWallet extends CoinServiceAPI { NodeModel? _ethNode; final _gasLimit = 21000; - final _blockExplorer = "https://blockscout.com/eth/mainnet/api?"; + // final _blockExplorer = "https://blockscout.com/eth/mainnet/api?"; + final _blockExplorer = "https://api.etherscan.io/api?"; final _gasTrackerUrl = "https://beaconcha.in/api/v1/execution/gasnow"; + @override + String get walletId => _walletId; + late String _walletId; + + late String _walletName; + late Coin _coin; + Timer? timer; + Timer? _networkAliveTimer; + @override set isFavorite(bool markFavorite) { DB.instance.put( @@ -140,7 +150,6 @@ class EthereumWallet extends CoinServiceAPI { _walletId = walletId; _walletName = walletName; _coin = coin; - _priceAPI = priceAPI ?? PriceAPI(Client()); _secureStore = secureStore; } @@ -167,11 +176,6 @@ class EthereumWallet extends CoinServiceAPI { @override String get walletName => _walletName; - late String _walletName; - - late Coin _coin; - Timer? timer; - Timer? _networkAliveTimer; @override Future> get allOwnAddresses => @@ -339,7 +343,9 @@ class EthereumWallet extends CoinServiceAPI { await DB.instance .put(boxName: walletId, key: "id", value: _walletId); await DB.instance.put( - boxName: walletId, key: 'receivingAddresses', value: ["0"]); + boxName: walletId, + key: 'receivingAddresses', + value: [_credentials.address.toString()]); await DB.instance .put(boxName: walletId, key: "receivingIndex", value: 0); await DB.instance @@ -480,6 +486,46 @@ class EthereumWallet extends CoinServiceAPI { _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( + _credentials.address.toString(), "tokentx"); + var tokenMap = {}; + 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] = {}; + tokenMap[key]["balance"] = 0; + + if (tokenMap.containsKey(key)) { + tokenMap[key]["contractAddress"] = element["contractAddress"]; + tokenMap[key]["decimals"] = element["tokenDecimal"]; + tokenMap[key]["name"] = element["tokenName"]; + tokenMap[key]["symbol"] = element["tokenSymbol"]; + if (element["to"] == _credentials.address.toString()) { + tokenMap[key]["balance"] += int.parse(element["value"] as String); + } else { + tokenMap[key]["balance"] -= int.parse(element["value"] as String); + } + } + }); + + tokenMap.forEach((key, value) { + //Create New token + + tokensList.add(value as Map); + }); + + await _secureStore.write( + 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 @@ -528,8 +574,8 @@ class EthereumWallet extends CoinServiceAPI { } if (!needsRefresh) { var allOwnAddresses = await _fetchAllOwnAddresses(); - AddressTransaction addressTransactions = - await fetchAddressTransactions(allOwnAddresses.elementAt(0)); + AddressTransaction addressTransactions = await fetchAddressTransactions( + allOwnAddresses.elementAt(0), "txlist"); final txData = await transactionData; if (addressTransactions.message == "OK") { final allTxs = addressTransactions.result; @@ -854,9 +900,10 @@ class EthereumWallet extends CoinServiceAPI { return isValidEthereumAddress(address); } - Future fetchAddressTransactions(String address) async { + Future fetchAddressTransactions( + String address, String action) async { final response = await get(Uri.parse( - "${_blockExplorer}module=account&action=txlist&address=$address")); + "${_blockExplorer}module=account&action=$action&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); if (response.statusCode == 200) { return AddressTransaction.fromJson( @@ -881,7 +928,8 @@ class EthereumWallet extends CoinServiceAPI { Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; final List> midSortedArray = []; - AddressTransaction txs = await fetchAddressTransactions(thisAddress); + AddressTransaction txs = + await fetchAddressTransactions(thisAddress, "txlist"); if (txs.message == "OK") { final allTxs = txs.result; @@ -991,13 +1039,11 @@ class EthereumWallet extends CoinServiceAPI { return txModel; } - @override - String get walletId => _walletId; - late String _walletId; - @override set walletName(String newName) => _walletName = newName; + // Future + void stopNetworkAlivePinging() { _networkAliveTimer?.cancel(); _networkAliveTimer = null; diff --git a/lib/services/tokens.dart b/lib/services/tokens.dart new file mode 100644 index 000000000..d7997d9ed --- /dev/null +++ b/lib/services/tokens.dart @@ -0,0 +1,374 @@ +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 new file mode 100644 index 000000000..ef3bfc17f --- /dev/null +++ b/lib/services/tokens/ethereum/ethereum_token.dart @@ -0,0 +1,127 @@ +import 'package:decimal/decimal.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/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/services/transaction_notification_tracker.dart'; + +class EthereumToken extends TokenServiceAPI { + @override + late bool shouldAutoSync; + late String _walletId; + late String _contractAddress; + late SecureStorageInterface _secureStore; + late final TransactionNotificationTracker txTracker; + + EthereumToken({ + required String contractAddress, + required String walletId, + required SecureStorageInterface secureStore, + required TransactionNotificationTracker tracker, + }) { + txTracker = tracker; + _walletId = walletId; + _contractAddress = contractAddress; + _secureStore = secureStore; + } + + @override + // TODO: implement allOwnAddresses + Future> get allOwnAddresses => throw UnimplementedError(); + + @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(); + } + + @override + // TODO: implement currentReceivingAddress + Future get currentReceivingAddress => throw UnimplementedError(); + + @override + Future estimateFeeFor(int satoshiAmount, int feeRate) { + // TODO: implement estimateFeeFor + throw UnimplementedError(); + } + + @override + // TODO: implement fees + Future get fees => throw UnimplementedError(); + + @override + Future initializeExisting() { + // TODO: implement initializeExisting + throw UnimplementedError(); + } + + @override + Future initializeNew() async { + throw UnimplementedError(); + } + + @override + // TODO: implement isConnected + bool get isConnected => throw UnimplementedError(); + + @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(); + + @override + Future> prepareSend( + {required String address, + required int satoshiAmount, + Map? args}) { + // TODO: implement prepareSend + throw UnimplementedError(); + } + + @override + Future refresh() { + // TODO: implement refresh + throw UnimplementedError(); + } + + @override + // TODO: implement totalBalance + Future get totalBalance => throw UnimplementedError(); + + @override + // TODO: implement transactionData + Future get transactionData => throw UnimplementedError(); + + @override + Future updateSentCachedTxData(Map txData) { + // TODO: implement updateSentCachedTxData + throw UnimplementedError(); + } + + @override + bool validateAddress(String address) { + // TODO: implement validateAddress + throw UnimplementedError(); + } +} diff --git a/lib/services/tokens/token_manager.dart b/lib/services/tokens/token_manager.dart new file mode 100644 index 000000000..cceee7e47 --- /dev/null +++ b/lib/services/tokens/token_manager.dart @@ -0,0 +1,130 @@ +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 new file mode 100644 index 000000000..0f23054f5 --- /dev/null +++ b/lib/services/tokens/token_service.dart @@ -0,0 +1,73 @@ +import 'package:decimal/decimal.dart'; +import 'package:stackwallet/models/models.dart'; +import 'package:stackwallet/services/tokens/ethereum/ethereum_token.dart'; +import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/prefs.dart'; + +abstract class TokenServiceAPI { + TokenServiceAPI(); + + factory TokenServiceAPI.from( + String contractAddress, + String walletId, + SecureStorageInterface secureStorageInterface, + TransactionNotificationTracker tracker, + Prefs prefs, + ) { + return EthereumToken( + contractAddress: contractAddress, + walletId: walletId, + secureStore: secureStorageInterface, + tracker: tracker, + ); + } + + Coin get coin; + bool get isRefreshing; + bool get shouldAutoSync; + set shouldAutoSync(bool shouldAutoSync); + + Future> prepareSend({ + required String address, + required int satoshiAmount, + Map? args, + }); + + Future confirmSend({required Map txData}); + + Future get fees; + Future get maxFee; + + Future get currentReceivingAddress; + // Future get currentLegacyReceivingAddress; + + Future get availableBalance; + Future get pendingBalance; + Future get totalBalance; + Future get balanceMinusMaxFee; + + Future> get allOwnAddresses; + + Future get transactionData; + + Future refresh(); + + // String get walletName; + // String get walletId; + + bool validateAddress(String address); + + Future initializeNew(); + Future initializeExisting(); + + // void Function(bool isActive)? onIsActiveWalletChanged; + + bool get isConnected; + + Future estimateFeeFor(int satoshiAmount, int feeRate); + + // used for electrumx coins + Future updateSentCachedTxData(Map txData); +} diff --git a/lib/services/tokens_service.dart b/lib/services/tokens_service.dart new file mode 100644 index 000000000..3c21fe3ea --- /dev/null +++ b/lib/services/tokens_service.dart @@ -0,0 +1,432 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_libmonero/monero/monero.dart'; +import 'package:flutter_libmonero/wownero/wownero.dart'; +import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; +import 'package:stackwallet/services/notifications_service.dart'; +import 'package:stackwallet/services/trade_sent_from_stack_service.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:uuid/uuid.dart'; + +class TokenInfo { + final Coin coin; + final String walletId; + final String contractAddress; + + const TokenInfo( + {required this.coin, + required this.walletId, + required this.contractAddress}); + + factory TokenInfo.fromJson(Map jsonObject) { + return TokenInfo( + coin: Coin.values.byName(jsonObject["coin"] as String), + walletId: jsonObject["id"] as String, + contractAddress: jsonObject["contractAddress"] as String, + ); + } + + Map toMap() { + return { + "contractAddress": contractAddress, + "walletId": walletId, + "coin": coin.name, + }; + } + + String toJsonString() { + return jsonEncode(toMap()); + } + + @override + String toString() { + return "TokenInfo: ${toJsonString()}"; + } +} + +class TokensService extends ChangeNotifier { + late final SecureStorageInterface _secureStore; + + // Future>? _walletNames; + // Future> get walletNames => + // _walletNames ??= _fetchWalletNames(); + + TokensService({ + required SecureStorageInterface secureStorageInterface, + }) { + _secureStore = secureStorageInterface; + } + + // Future getWalletCryptoCurrency({required String walletName}) async { + // final id = await getWalletId(walletName); + // final currency = DB.instance.get( + // boxName: DB.boxNameAllWalletsData, key: "${id}_cryptoCurrency"); + // return Coin.values.byName(currency as String); + // } + + // Future renameWallet({ + // required String from, + // required String to, + // required bool shouldNotifyListeners, + // }) async { + // if (from == to) { + // return true; + // } + // + // final walletInfo = DB.instance + // .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map; + // + // final info = walletInfo.values.firstWhere( + // (element) => element['name'] == from, + // orElse: () => {}) as Map; + // + // if (info.isEmpty) { + // // tried to rename a non existing wallet + // Logging.instance + // .log("Tried to rename a non existing wallet!", level: LogLevel.Error); + // return false; + // } + // + // if (from != to && + // (walletInfo.values.firstWhere((element) => element['name'] == to, + // orElse: () => {}) as Map) + // .isNotEmpty) { + // // name already exists + // Logging.instance.log("wallet with name \"$to\" already exists!", + // level: LogLevel.Error); + // return false; + // } + // + // info["name"] = to; + // walletInfo[info['id']] = info; + // + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, key: 'names', value: walletInfo); + // await refreshWallets(shouldNotifyListeners); + // return true; + // } + + // Future> _fetchWalletNames() async { + // final names = DB.instance + // .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map?; + // if (names == null) { + // Logging.instance.log( + // "Fetched wallet 'names' returned null. Setting initializing 'names'", + // level: LogLevel.Info); + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, + // key: 'names', + // value: {}); + // return {}; + // } + // Logging.instance.log("Fetched wallet names: $names", level: LogLevel.Info); + // final mapped = Map.from(names); + // mapped.removeWhere((name, dyn) { + // final jsonObject = Map.from(dyn as Map); + // try { + // Coin.values.byName(jsonObject["coin"] as String); + // return false; + // } catch (e, s) { + // Logging.instance.log("Error, ${jsonObject["coin"]} does not exist", + // level: LogLevel.Error); + // return true; + // } + // }); + // + // return mapped.map((name, dyn) => MapEntry( + // name, WalletInfo.fromJson(Map.from(dyn as Map)))); + // } + + // Future addExistingStackWallet({ + // required String name, + // required String walletId, + // required Coin coin, + // required bool shouldNotifyListeners, + // }) async { + // final _names = DB.instance + // .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map?; + // + // Map names; + // if (_names == null) { + // names = {}; + // } else { + // names = Map.from(_names); + // } + // + // if (names.keys.contains(walletId)) { + // throw Exception("Wallet with walletId \"$walletId\" already exists!"); + // } + // if (names.values.where((element) => element['name'] == name).isNotEmpty) { + // throw Exception("Wallet with name \"$name\" already exists!"); + // } + // + // names[walletId] = { + // "id": walletId, + // "coin": coin.name, + // "name": name, + // }; + // + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, key: 'names', value: names); + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, + // key: "${walletId}_cryptoCurrency", + // value: coin.name); + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, + // key: "${walletId}_mnemonicHasBeenVerified", + // value: false); + // await DB.instance.addWalletBox(walletId: walletId); + // await refreshWallets(shouldNotifyListeners); + // } + + // /// returns the new walletId if successful, otherwise null + // Future addNewWallet({ + // required String name, + // required Coin coin, + // required bool shouldNotifyListeners, + // }) async { + // final _names = DB.instance + // .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map?; + // + // Map names; + // if (_names == null) { + // names = {}; + // } else { + // names = Map.from(_names); + // } + // + // // Prevent overwriting or storing empty names + // if (name.isEmpty || + // names.values.where((element) => element['name'] == name).isNotEmpty) { + // return null; + // } + // + // final id = const Uuid().v1(); + // names[id] = { + // "id": id, + // "coin": coin.name, + // "name": name, + // }; + // + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, key: 'names', value: names); + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, + // key: "${id}_cryptoCurrency", + // value: coin.name); + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, + // key: "${id}_mnemonicHasBeenVerified", + // value: false); + // await DB.instance.addWalletBox(walletId: id); + // await refreshWallets(shouldNotifyListeners); + // return id; + // } + + // Future> getFavoriteWalletIds() async { + // return DB.instance + // .values(boxName: DB.boxNameFavoriteWallets) + // .toList(); + // } + + // Future saveFavoriteWalletIds(List walletIds) async { + // await DB.instance.deleteAll(boxName: DB.boxNameFavoriteWallets); + // await DB.instance + // .addAll(boxName: DB.boxNameFavoriteWallets, values: walletIds); + // debugPrint("saveFavoriteWalletIds list: $walletIds"); + // } + // + // Future addFavorite(String walletId) async { + // final list = await getFavoriteWalletIds(); + // if (!list.contains(walletId)) { + // list.add(walletId); + // } + // await saveFavoriteWalletIds(list); + // } + // + // Future removeFavorite(String walletId) async { + // final list = await getFavoriteWalletIds(); + // list.remove(walletId); + // await saveFavoriteWalletIds(list); + // } + // + // Future moveFavorite({ + // required int fromIndex, + // required int toIndex, + // }) async { + // final list = await getFavoriteWalletIds(); + // if (fromIndex < toIndex) { + // toIndex -= 1; + // } + // final walletId = list.removeAt(fromIndex); + // list.insert(toIndex, walletId); + // await saveFavoriteWalletIds(list); + // } + // + // Future checkForDuplicate(String name) async { + // final names = DB.instance + // .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map?; + // if (names == null) return false; + // + // return names.values.where((element) => element['name'] == name).isNotEmpty; + // } + + Future getWalletId(String walletName) async { + final names = DB.instance + .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map; + final shells = + names.values.where((element) => element['name'] == walletName); + if (shells.isEmpty) { + return null; + } + return shells.first["id"] as String; + } + + // Future isMnemonicVerified({required String walletId}) async { + // final isVerified = DB.instance.get( + // boxName: DB.boxNameAllWalletsData, + // key: "${walletId}_mnemonicHasBeenVerified") as bool?; + // + // if (isVerified == null) { + // Logging.instance.log( + // "isMnemonicVerified(walletId: $walletId) returned null which should never happen!", + // level: LogLevel.Error, + // ); + // throw Exception( + // "isMnemonicVerified(walletId: $walletId) returned null which should never happen!"); + // } else { + // return isVerified; + // } + // } + // + // Future setMnemonicVerified({required String walletId}) async { + // final isVerified = DB.instance.get( + // boxName: DB.boxNameAllWalletsData, + // key: "${walletId}_mnemonicHasBeenVerified") as bool?; + // + // if (isVerified == null) { + // Logging.instance.log( + // "setMnemonicVerified(walletId: $walletId) tried running on non existent wallet!", + // level: LogLevel.Error, + // ); + // throw Exception( + // "setMnemonicVerified(walletId: $walletId) tried running on non existent wallet!"); + // } else if (isVerified) { + // Logging.instance.log( + // "setMnemonicVerified(walletId: $walletId) tried running on already verified wallet!", + // level: LogLevel.Error, + // ); + // throw Exception( + // "setMnemonicVerified(walletId: $walletId) tried running on already verified wallet!"); + // } else { + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, + // key: "${walletId}_mnemonicHasBeenVerified", + // value: true); + // Logging.instance.log( + // "setMnemonicVerified(walletId: $walletId) successful", + // level: LogLevel.Error, + // ); + // } + // } + // + // // pin + mnemonic as well as anything else in secureStore + // Future deleteWallet(String name, bool shouldNotifyListeners) async { + // final names = DB.instance.get( + // boxName: DB.boxNameAllWalletsData, key: 'names') as Map? ?? + // {}; + // + // final walletId = await getWalletId(name); + // if (walletId == null) { + // return 3; + // } + // + // Logging.instance.log( + // "deleteWallet called with name=$name and id=$walletId", + // level: LogLevel.Warning, + // ); + // + // final shell = names.remove(walletId); + // + // if (shell == null) { + // return 0; + // } + // + // // TODO delete derivations!!! + // await _secureStore.delete(key: "${walletId}_pin"); + // await _secureStore.delete(key: "${walletId}_mnemonic"); + // + // await DB.instance.delete( + // boxName: DB.boxNameAllWalletsData, key: "${walletId}_cryptoCurrency"); + // await DB.instance.delete( + // boxName: DB.boxNameAllWalletsData, + // key: "${walletId}_mnemonicHasBeenVerified"); + // if (coinFromPrettyName(shell['coin'] as String) == Coin.wownero) { + // final wowService = + // wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); + // await wowService.remove(walletId); + // Logging.instance + // .log("monero wallet: $walletId deleted", level: LogLevel.Info); + // } else if (coinFromPrettyName(shell['coin'] as String) == Coin.monero) { + // final xmrService = + // monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); + // await xmrService.remove(walletId); + // Logging.instance + // .log("monero wallet: $walletId deleted", level: LogLevel.Info); + // } else if (coinFromPrettyName(shell['coin'] as String) == Coin.epicCash) { + // final deleteResult = + // await deleteEpicWallet(walletId: walletId, secureStore: _secureStore); + // Logging.instance.log( + // "epic wallet: $walletId deleted with result: $deleteResult", + // level: LogLevel.Info); + // } + // + // // box data may currently still be read/written to if wallet was refreshing + // // when delete was requested so instead of deleting now we mark the wallet + // // as needs delete by adding it's id to a list which gets checked on app start + // await DB.instance.add( + // boxName: DB.boxNameWalletsToDeleteOnStart, value: walletId); + // + // final lookupService = TradeSentFromStackService(); + // for (final lookup in lookupService.all) { + // if (lookup.walletIds.contains(walletId)) { + // // update lookup data to reflect deleted wallet + // await lookupService.save( + // tradeWalletLookup: lookup.copyWith( + // walletIds: lookup.walletIds.where((id) => id != walletId).toList(), + // ), + // ); + // } + // } + // + // // delete notifications tied to deleted wallet + // for (final notification in NotificationsService.instance.notifications) { + // if (notification.walletId == walletId) { + // await NotificationsService.instance.delete(notification, false); + // } + // } + // + // if (names.isEmpty) { + // await DB.instance.deleteAll(boxName: DB.boxNameAllWalletsData); + // _walletNames = Future(() => {}); + // notifyListeners(); + // return 2; // error code no wallets on device + // } + // + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, key: 'names', value: names); + // await refreshWallets(shouldNotifyListeners); + // return 0; + // } + // + // Future refreshWallets(bool shouldNotifyListeners) async { + // final newNames = await _fetchWalletNames(); + // _walletNames = Future(() => newNames); + // if (shouldNotifyListeners) notifyListeners(); + // } +}