convert token service to change notifier, add token cache per eth wallet, token balances, and fix routing issues

This commit is contained in:
julian 2023-02-27 10:01:06 -06:00
parent 2287cd751e
commit fcd8f01d93
7 changed files with 197 additions and 156 deletions

View file

@ -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,
);
}
}

View file

@ -15,7 +15,6 @@ import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:tuple/tuple.dart';
class MyTokenSelectItem extends ConsumerWidget { class MyTokenSelectItem extends ConsumerWidget {
const MyTokenSelectItem( const MyTokenSelectItem(
@ -51,27 +50,25 @@ class MyTokenSelectItem extends ConsumerWidget {
BorderRadius.circular(Constants.size.circularBorderRadius), BorderRadius.circular(Constants.size.circularBorderRadius),
), ),
onPressed: () async { onPressed: () async {
final tokenService = EthereumTokenService( ref.read(tokenServiceStateProvider.state).state =
EthereumTokenService(
token: token, token: token,
secureStore: ref.read(secureStoreProvider), secureStore: ref.read(secureStoreProvider),
ethWallet: ref.read(managerProvider).wallet as EthereumWallet, ethWallet: ref.read(managerProvider).wallet as EthereumWallet,
tracker: TransactionNotificationTracker( tracker: TransactionNotificationTracker(
walletId: ref.read(managerProvider).walletId), walletId: ref.read(managerProvider).walletId,
),
); );
await showLoading<void>( await showLoading<void>(
whileFuture: tokenService.initializeExisting(), whileFuture: ref.read(tokenServiceProvider)!.initializeExisting(),
context: context, context: context,
message: "Loading ${token.name}", message: "Loading ${token.name}",
); );
await Navigator.of(context).pushNamed( await Navigator.of(context).pushNamed(
TokenView.routeName, TokenView.routeName,
arguments: Tuple3( arguments: walletId,
walletId,
token,
tokenService,
),
); );
}, },

View file

@ -1,127 +1,50 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/pages/token_view/token_view.dart';
import 'package:stackwallet/pages/token_view/sub_widgets/token_summary_info.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_summary_info.dart'; import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
class TokenSummary extends StatelessWidget { class TokenSummary extends ConsumerWidget {
const TokenSummary({ const TokenSummary({
Key? key, Key? key,
required this.walletId, 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); }) : super(key: key);
final String walletId; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
return AspectRatio( return RoundedContainer(
aspectRatio: aspectRatio, color: const Color(0xFFE9EAFF), // todo: fix color
child: ConstrainedBox( // color: Theme.of(context).extension<StackColors>()!.,
constraints: BoxConstraints(
minHeight: minHeight, child: Column(
minWidth: minWidth, children: [
maxHeight: maxHeight, Text(
maxWidth: minWidth, ref.watch(
), walletsChangeNotifierProvider.select(
child: Stack( (value) => value.getManager(walletId).walletName,
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,
),
],
), ),
), ),
// Positioned.fill( style: STextStyles.label(context),
// child: ),
// Column( Text(
// mainAxisAlignment: MainAxisAlignment.end, ref.watch(tokenServiceProvider.select((value) => value!.balance
// children: [ .getTotal()
Align( .toStringAsFixed(ref.watch(tokenServiceProvider
alignment: Alignment.bottomCenter, .select((value) => value!.token.decimals))))),
child: Row( style: STextStyles.label(context),
children: [ ),
const Spacer( Text(
flex: 1, ref.watch(
), walletsChangeNotifierProvider.select(
Expanded( (value) => value.getManager(walletId).walletName,
flex: 3,
child: SvgPicture.asset(
Assets.svg.ellipse2,
// fit: BoxFit.f,
// clipBehavior: Clip.none,
),
),
const SizedBox(
width: 13,
),
],
), ),
), ),
// ], style: STextStyles.label(context),
// ), ),
// ), ],
Positioned.fill(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: TokenSummaryInfo(
walletId: walletId,
managerProvider: managerProvider,
initialSyncStatus: initialSyncStatus,
),
),
),
],
),
), ),
); );
} }

View file

@ -2,7 +2,6 @@ import 'package:event_bus/event_bus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.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/token_view/sub_widgets/token_transaction_list_widget.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart';
import 'package:stackwallet/services/ethereum/ethereum_token_service.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/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/blue_text_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 /// [eventBus] should only be set during testing
class TokenView extends ConsumerStatefulWidget { class TokenView extends ConsumerStatefulWidget {
const TokenView({ const TokenView({
Key? key, Key? key,
required this.walletId, required this.walletId,
required this.token,
required this.tokenService,
this.eventBus, this.eventBus,
}) : super(key: key); }) : super(key: key);
@ -29,8 +31,6 @@ class TokenView extends ConsumerStatefulWidget {
static const double navBarHeight = 65.0; static const double navBarHeight = 65.0;
final String walletId; final String walletId;
final EthToken token;
final EthereumTokenService tokenService;
final EventBus? eventBus; final EventBus? eventBus;
@override @override
@ -51,7 +51,6 @@ class _TokenViewState extends ConsumerState<TokenView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType"); debugPrint("BUILD: $runtimeType");
// print("MY TOTAL BALANCE IS ${widget.token.totalBalance}");
return Background( return Background(
child: Scaffold( child: Scaffold(
@ -67,7 +66,6 @@ class _TokenViewState extends ConsumerState<TokenView> {
children: [ children: [
SvgPicture.asset( SvgPicture.asset(
Assets.svg.iconFor(coin: Coin.ethereum), Assets.svg.iconFor(coin: Coin.ethereum),
// color: Theme.of(context).extension<StackColors>()!.accentColorDark
width: 24, width: 24,
height: 24, height: 24,
), ),
@ -76,14 +74,16 @@ class _TokenViewState extends ConsumerState<TokenView> {
), ),
Expanded( Expanded(
child: Text( child: Text(
widget.token.name, ref.watch(tokenServiceProvider
.select((value) => value!.token.name)),
style: STextStyles.navBarTitle(context), style: STextStyles.navBarTitle(context),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
Expanded( Expanded(
child: Text( child: Text(
widget.token.symbol, ref.watch(tokenServiceProvider
.select((value) => value!.token.symbol)),
style: STextStyles.navBarTitle(context), style: STextStyles.navBarTitle(context),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -167,7 +167,8 @@ class _TokenViewState extends ConsumerState<TokenView> {
children: [ children: [
Expanded( Expanded(
child: TokenTransactionsList( child: TokenTransactionsList(
tokenService: widget.tokenService, tokenService: ref.watch(tokenServiceProvider
.select((value) => value!)),
walletId: widget.walletId, walletId: widget.walletId,
), ),
), ),

View file

@ -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/security_settings.dart';
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/syncing_preferences_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/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/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_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'; import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart';
@ -1461,14 +1460,11 @@ class RouteGenerator {
// } // }
case TokenView.routeName: case TokenView.routeName:
if (args is Tuple3<String, EthToken, if (args is String) {
EthereumTokenService>) {
return getRoute( return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute, shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => TokenView( builder: (_) => TokenView(
walletId: args.item1, walletId: args,
token: args.item2,
tokenService: args.item3,
), ),
settings: RouteSettings( settings: RouteSettings(
name: settings.name, name: settings.name,

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:decimal/decimal.dart'; import 'package:decimal/decimal.dart';
import 'package:ethereum_addresses/ethereum_addresses.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:stackwallet/models/ethereum/eth_token.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/isar/models/blockchain_data/transaction.dart';
import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/paymint/fee_object_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/coins/ethereum/ethereum_wallet.dart';
import 'package:stackwallet/services/ethereum/ethereum_api.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/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.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/node_service.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/default_nodes.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:tuple/tuple.dart';
import 'package:web3dart/web3dart.dart' as web3dart; import 'package:web3dart/web3dart.dart' as web3dart;
class EthereumTokenService { class EthereumTokenService extends ChangeNotifier with EthTokenCache {
final EthToken token; final EthToken token;
final EthereumWallet ethWallet; final EthereumWallet ethWallet;
final TransactionNotificationTracker tracker; final TransactionNotificationTracker tracker;
@ -48,11 +51,11 @@ class EthereumTokenService {
required this.tracker, required this.tracker,
}) : _secureStore = secureStore { }) : _secureStore = secureStore {
_contractAddress = web3dart.EthereumAddress.fromHex(token.contractAddress); _contractAddress = web3dart.EthereumAddress.fromHex(token.contractAddress);
initCache(ethWallet.walletId, token);
} }
Future<Decimal> get availableBalance async { TokenBalance get balance => _balance ??= getCachedBalance();
return await totalBalance; TokenBalance? _balance;
}
Coin get coin => Coin.ethereum; Coin get coin => Coin.ethereum;
@ -177,18 +180,6 @@ class EthereumTokenService {
final feeEstimate = await estimateFeeFor(satoshiAmount, fee); 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 = { Map<String, dynamic> txData = {
"fee": feeEstimate, "fee": feeEstimate,
"feeInWei": fee, "feeInWei": fee,
@ -205,6 +196,7 @@ class EthereumTokenService {
if (!_refreshLock) { if (!_refreshLock) {
_refreshLock = true; _refreshLock = true;
try { try {
await refreshCachedBalance();
await _refreshTransactions(); await _refreshTransactions();
} catch (e, s) { } catch (e, s) {
Logging.instance.log( Logging.instance.log(
@ -213,22 +205,33 @@ class EthereumTokenService {
); );
} finally { } finally {
_refreshLock = false; _refreshLock = false;
notifyListeners();
} }
} }
} }
Future<Decimal> get totalBalance async { Future<void> refreshCachedBalance() async {
final balanceRequest = await _client.call( final balanceRequest = await _client.call(
contract: _contract, contract: _contract,
function: _balanceFunction, function: _balanceFunction,
params: [_credentials.address]); params: [_credentials.address]);
String balance = balanceRequest.first.toString(); print("==========================================");
final balanceInDecimal = Format.satoshisToEthTokenAmount( print("balanceRequest: $balanceRequest");
int.parse(balance), print("==========================================");
token.decimals,
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 Future<List<Transaction>> get transactions => ethWallet.db

View file

@ -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(),
);
}
}