diff --git a/lib/models/token_balance.dart b/lib/models/token_balance.dart new file mode 100644 index 000000000..632a5ade5 --- /dev/null +++ b/lib/models/token_balance.dart @@ -0,0 +1,71 @@ +import 'dart:convert'; + +import 'package:decimal/decimal.dart'; +import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/format.dart'; + +class TokenBalance extends Balance { + TokenBalance({ + required this.contractAddress, + required this.decimalPlaces, + required super.total, + required super.spendable, + required super.blockedTotal, + required super.pendingSpendable, + super.coin = Coin.ethereum, + }); + + final String contractAddress; + final int decimalPlaces; + + @override + Decimal getTotal({bool includeBlocked = false}) => + Format.satoshisToEthTokenAmount( + includeBlocked ? total : total - blockedTotal, + decimalPlaces, + ); + + @override + Decimal getSpendable() => Format.satoshisToEthTokenAmount( + spendable, + decimalPlaces, + ); + + @override + Decimal getPending() => Format.satoshisToEthTokenAmount( + pendingSpendable, + decimalPlaces, + ); + + @override + Decimal getBlocked() => Format.satoshisToEthTokenAmount( + blockedTotal, + decimalPlaces, + ); + + @override + String toJsonIgnoreCoin() => jsonEncode({ + "decimalPlaces": decimalPlaces, + "total": total, + "spendable": spendable, + "blockedTotal": blockedTotal, + "pendingSpendable": pendingSpendable, + }); + + factory TokenBalance.fromJson( + String json, + String contractAddress, + int decimalPlaces, + ) { + final decoded = jsonDecode(json); + return TokenBalance( + contractAddress: contractAddress, + decimalPlaces: decoded["decimalPlaces"] as int, + total: decoded["total"] as int, + spendable: decoded["spendable"] as int, + blockedTotal: decoded["blockedTotal"] as int, + pendingSpendable: decoded["pendingSpendable"] as int, + ); + } +} 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 c7bfeb25a..3d021f30e 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 @@ -15,7 +15,6 @@ import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import 'package:tuple/tuple.dart'; class MyTokenSelectItem extends ConsumerWidget { const MyTokenSelectItem( @@ -51,27 +50,25 @@ class MyTokenSelectItem extends ConsumerWidget { BorderRadius.circular(Constants.size.circularBorderRadius), ), onPressed: () async { - final tokenService = EthereumTokenService( + ref.read(tokenServiceStateProvider.state).state = + EthereumTokenService( token: token, secureStore: ref.read(secureStoreProvider), ethWallet: ref.read(managerProvider).wallet as EthereumWallet, tracker: TransactionNotificationTracker( - walletId: ref.read(managerProvider).walletId), + walletId: ref.read(managerProvider).walletId, + ), ); await showLoading<void>( - whileFuture: tokenService.initializeExisting(), + whileFuture: ref.read(tokenServiceProvider)!.initializeExisting(), context: context, message: "Loading ${token.name}", ); await Navigator.of(context).pushNamed( TokenView.routeName, - arguments: Tuple3( - walletId, - token, - tokenService, - ), + arguments: walletId, ); }, diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart index bac284bb0..28abdda8a 100644 --- a/lib/pages/token_view/sub_widgets/token_summary.dart +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -1,127 +1,50 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:stackwallet/pages/token_view/sub_widgets/token_summary_info.dart'; -import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_summary_info.dart'; -import 'package:stackwallet/services/coins/manager.dart'; -import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; -import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; -class TokenSummary extends StatelessWidget { +class TokenSummary extends ConsumerWidget { const TokenSummary({ Key? key, required this.walletId, - required this.managerProvider, - required this.initialSyncStatus, - this.aspectRatio = 2.0, - this.minHeight = 100.0, - this.minWidth = 200.0, - this.maxHeight = 250.0, - this.maxWidth = 400.0, }) : super(key: key); final String walletId; - final ChangeNotifierProvider<Manager> managerProvider; - final WalletSyncStatus initialSyncStatus; - - final double aspectRatio; - final double minHeight; - final double minWidth; - final double maxHeight; - final double maxWidth; @override - Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: aspectRatio, - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: minHeight, - minWidth: minWidth, - maxHeight: maxHeight, - maxWidth: minWidth, - ), - child: Stack( - children: [ - Consumer( - builder: (_, ref, __) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context) - .extension<StackColors>()! - .colorForCoin(ref.watch( - managerProvider.select((value) => value.coin))), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - ); - }, - ), - Positioned.fill( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Spacer( - flex: 5, - ), - Expanded( - flex: 6, - child: SvgPicture.asset( - Assets.svg.ellipse1, - // fit: BoxFit.fitWidth, - // clipBehavior: Clip.none, - ), - ), - const SizedBox( - width: 25, - ), - ], + Widget build(BuildContext context, WidgetRef ref) { + return RoundedContainer( + color: const Color(0xFFE9EAFF), // todo: fix color + // color: Theme.of(context).extension<StackColors>()!., + + child: Column( + children: [ + Text( + ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(walletId).walletName, ), ), - // Positioned.fill( - // child: - // Column( - // mainAxisAlignment: MainAxisAlignment.end, - // children: [ - Align( - alignment: Alignment.bottomCenter, - child: Row( - children: [ - const Spacer( - flex: 1, - ), - Expanded( - flex: 3, - child: SvgPicture.asset( - Assets.svg.ellipse2, - // fit: BoxFit.f, - // clipBehavior: Clip.none, - ), - ), - const SizedBox( - width: 13, - ), - ], + style: STextStyles.label(context), + ), + Text( + ref.watch(tokenServiceProvider.select((value) => value!.balance + .getTotal() + .toStringAsFixed(ref.watch(tokenServiceProvider + .select((value) => value!.token.decimals))))), + style: STextStyles.label(context), + ), + Text( + ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(walletId).walletName, ), ), - // ], - // ), - // ), - Positioned.fill( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: TokenSummaryInfo( - walletId: walletId, - managerProvider: managerProvider, - initialSyncStatus: initialSyncStatus, - ), - ), - ), - ], - ), + style: STextStyles.label(context), + ), + ], ), ); } diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 41357ff37..4160650bf 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -2,7 +2,6 @@ import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/models/ethereum/eth_token.dart'; import 'package:stackwallet/pages/token_view/sub_widgets/token_transaction_list_widget.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; @@ -15,13 +14,16 @@ import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +final tokenServiceStateProvider = + StateProvider<EthereumTokenService?>((ref) => null); +final tokenServiceProvider = ChangeNotifierProvider<EthereumTokenService?>( + (ref) => ref.watch(tokenServiceStateProvider)); + /// [eventBus] should only be set during testing class TokenView extends ConsumerStatefulWidget { const TokenView({ Key? key, required this.walletId, - required this.token, - required this.tokenService, this.eventBus, }) : super(key: key); @@ -29,8 +31,6 @@ class TokenView extends ConsumerStatefulWidget { static const double navBarHeight = 65.0; final String walletId; - final EthToken token; - final EthereumTokenService tokenService; final EventBus? eventBus; @override @@ -51,7 +51,6 @@ class _TokenViewState extends ConsumerState<TokenView> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - // print("MY TOTAL BALANCE IS ${widget.token.totalBalance}"); return Background( child: Scaffold( @@ -67,7 +66,6 @@ class _TokenViewState extends ConsumerState<TokenView> { children: [ SvgPicture.asset( Assets.svg.iconFor(coin: Coin.ethereum), - // color: Theme.of(context).extension<StackColors>()!.accentColorDark width: 24, height: 24, ), @@ -76,14 +74,16 @@ class _TokenViewState extends ConsumerState<TokenView> { ), Expanded( child: Text( - widget.token.name, + ref.watch(tokenServiceProvider + .select((value) => value!.token.name)), style: STextStyles.navBarTitle(context), overflow: TextOverflow.ellipsis, ), ), Expanded( child: Text( - widget.token.symbol, + ref.watch(tokenServiceProvider + .select((value) => value!.token.symbol)), style: STextStyles.navBarTitle(context), overflow: TextOverflow.ellipsis, ), @@ -167,7 +167,8 @@ class _TokenViewState extends ConsumerState<TokenView> { children: [ Expanded( child: TokenTransactionsList( - tokenService: widget.tokenService, + tokenService: ref.watch(tokenServiceProvider + .select((value) => value!)), walletId: widget.walletId, ), ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 37d7ccefa..1b3c5b9ab 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -128,7 +128,6 @@ import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/nodes_ import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/security_settings.dart'; import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/syncing_preferences_settings.dart'; import 'package:stackwallet/services/coins/manager.dart'; -import 'package:stackwallet/services/ethereum/ethereum_token_service.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/utilities/enums/add_wallet_type_enum.dart'; @@ -1461,14 +1460,11 @@ class RouteGenerator { // } case TokenView.routeName: - if (args is Tuple3<String, EthToken, - EthereumTokenService>) { + if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => TokenView( - walletId: args.item1, - token: args.item2, - tokenService: args.item3, + walletId: args, ), settings: RouteSettings( name: settings.name, diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart index bae28dba9..ca26fbecd 100644 --- a/lib/services/ethereum/ethereum_token_service.dart +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:decimal/decimal.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; +import 'package:flutter/widgets.dart'; import 'package:http/http.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/models/ethereum/eth_token.dart'; @@ -9,10 +10,12 @@ import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/token_balance.dart'; import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/services/ethereum/ethereum_api.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/mixins/eth_token_cache.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; @@ -25,7 +28,7 @@ import 'package:stackwallet/utilities/logger.dart'; import 'package:tuple/tuple.dart'; import 'package:web3dart/web3dart.dart' as web3dart; -class EthereumTokenService { +class EthereumTokenService extends ChangeNotifier with EthTokenCache { final EthToken token; final EthereumWallet ethWallet; final TransactionNotificationTracker tracker; @@ -48,11 +51,11 @@ class EthereumTokenService { required this.tracker, }) : _secureStore = secureStore { _contractAddress = web3dart.EthereumAddress.fromHex(token.contractAddress); + initCache(ethWallet.walletId, token); } - Future<Decimal> get availableBalance async { - return await totalBalance; - } + TokenBalance get balance => _balance ??= getCachedBalance(); + TokenBalance? _balance; Coin get coin => Coin.ethereum; @@ -177,18 +180,6 @@ class EthereumTokenService { final feeEstimate = await estimateFeeFor(satoshiAmount, fee); - bool isSendAll = false; - final balance = - Format.decimalAmountToSatoshis(await availableBalance, coin); - if (satoshiAmount == balance) { - isSendAll = true; - } - - if (isSendAll) { - //Send the full balance - satoshiAmount = balance; - } - Map<String, dynamic> txData = { "fee": feeEstimate, "feeInWei": fee, @@ -205,6 +196,7 @@ class EthereumTokenService { if (!_refreshLock) { _refreshLock = true; try { + await refreshCachedBalance(); await _refreshTransactions(); } catch (e, s) { Logging.instance.log( @@ -213,22 +205,33 @@ class EthereumTokenService { ); } finally { _refreshLock = false; + notifyListeners(); } } } - Future<Decimal> get totalBalance async { + Future<void> refreshCachedBalance() async { final balanceRequest = await _client.call( contract: _contract, function: _balanceFunction, params: [_credentials.address]); - String balance = balanceRequest.first.toString(); - final balanceInDecimal = Format.satoshisToEthTokenAmount( - int.parse(balance), - token.decimals, + print("=========================================="); + print("balanceRequest: $balanceRequest"); + print("=========================================="); + + String _balance = balanceRequest.first.toString(); + + final newBalance = TokenBalance( + contractAddress: token.contractAddress, + total: int.parse(_balance), + spendable: int.parse(_balance), + blockedTotal: 0, + pendingSpendable: 0, + decimalPlaces: token.decimals, ); - return Decimal.parse(balanceInDecimal.toString()); + await updateCachedBalance(newBalance); + notifyListeners(); } Future<List<Transaction>> get transactions => ethWallet.db diff --git a/lib/services/mixins/eth_token_cache.dart b/lib/services/mixins/eth_token_cache.dart new file mode 100644 index 000000000..b490e849c --- /dev/null +++ b/lib/services/mixins/eth_token_cache.dart @@ -0,0 +1,50 @@ +import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/models/ethereum/eth_token.dart'; +import 'package:stackwallet/models/token_balance.dart'; + +abstract class _Keys { + static String tokenBalance(String contractAddress) { + return "tokenBalanceCache_$contractAddress"; + } +} + +mixin EthTokenCache { + late final String _walletId; + late final EthToken _token; + + void initCache(String walletId, EthToken token) { + _walletId = walletId; + _token = token; + } + + // token balance cache + TokenBalance getCachedBalance() { + final jsonString = DB.instance.get<dynamic>( + boxName: _walletId, + key: _Keys.tokenBalance(_token.contractAddress), + ) as String?; + if (jsonString == null) { + return TokenBalance( + contractAddress: _token.contractAddress, + decimalPlaces: _token.decimals, + total: 0, + spendable: 0, + blockedTotal: 0, + pendingSpendable: 0, + ); + } + return TokenBalance.fromJson( + jsonString, + _token.contractAddress, + _token.decimals, + ); + } + + Future<void> updateCachedBalance(TokenBalance balance) async { + await DB.instance.put<dynamic>( + boxName: _walletId, + key: _Keys.tokenBalance(_token.contractAddress), + value: balance.toJsonIgnoreCoin(), + ); + } +}