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

View file

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

View file

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

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/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,

View file

@ -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

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