Skip to content

Commit

Permalink
fix: add caching balances (#342)
Browse files Browse the repository at this point in the history
* fix: add caching balances

* fix: balances logging

* fix: update money_improver
  • Loading branch information
Alex-A4 authored Oct 25, 2023
1 parent 8c0ab66 commit 24239a7
Show file tree
Hide file tree
Showing 16 changed files with 305 additions and 26 deletions.
159 changes: 159 additions & 0 deletions lib/app/service/storage_service/balance_storage_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import 'dart:convert';

import 'package:app/app/service/service.dart';
import 'package:app/data/models/models.dart';
import 'package:encrypted_storage/encrypted_storage.dart';
import 'package:injectable/injectable.dart';
import 'package:logging/logging.dart';
import 'package:nekoton_repository/nekoton_repository.dart';
import 'package:rxdart/rxdart.dart';

const _overallBalancesDomain = 'overallBalancesDomain';
const _balancesDomain = 'balancesDomain';

@singleton
class BalanceStorageService extends AbstractStorageService {
BalanceStorageService(this._storage);

static final _logger = Logger('BalanceStorageService');

/// Storage that is used to store data
final EncryptedStorage _storage;

@override
Future<void> init() => Future.wait([
_streamedOverallBalance(),
_streamedBalance(),
]);

@override
Future<void> clearSensitiveData() => Future.wait([
deleteOverallBalances(),
]);

/// Subject for overall balances for accounts
/// key - address of account, value - overall balance in fiat
final _overallBalancesSubject =
BehaviorSubject<Map<Address, Fixed>>.seeded({});

/// Get cached overall balance for accounts
/// key - address of account, value - overall balance in fiat
Map<Address, Fixed> get overallBalance => _overallBalancesSubject.value;

/// Stream that allows tracking overall balance changing
Stream<Map<Address, Fixed>> get overallBalancesStream =>
_overallBalancesSubject;

/// Put overall balances into stream
Future<void> _streamedOverallBalance() async =>
_overallBalancesSubject.add(await readOverallBalances());

/// Get all overall balances for accounts
/// key - address, value - overall balance
Future<Map<Address, Fixed>> readOverallBalances() async {
final encoded = await _storage.getDomain(domain: _overallBalancesDomain);

return encoded.map(
(key, value) => MapEntry(
Address(address: key),
FixedImprover.fromJson(jsonDecode(value) as Map<String, dynamic>),
),
);
}

/// Set overall balance for specified account
Future<void> setOverallBalance({
required Address accountAddress,
required Fixed balance,
}) async {
try {
await _storage.set(
accountAddress.address,
jsonEncode(balance.toJson()),
domain: _overallBalancesDomain,
);
await _streamedOverallBalance();
} catch (e, t) {
_logger.severe('setOverallBalance', e, t);
}
}

/// Delete overall balances
Future<void> deleteOverallBalances() async {
await _storage.clearDomain(_overallBalancesDomain);
}

/// Subject for token balances for accounts
/// key - address of account, value - list of balances for tokens in scope of
/// this account.
final _balancesSubject =
BehaviorSubject<Map<Address, List<AccountBalanceModel>>>.seeded({});

/// Get cached token balances for accounts
/// key - address of account, value - list of balances for tokens in scope of
/// this account.
Map<Address, List<AccountBalanceModel>> get balances =>
_balancesSubject.value;

/// Stream that allows tracking token balances for accounts
Stream<Map<Address, List<AccountBalanceModel>>> get balancesStream =>
_balancesSubject;

/// Put token balances for accounts into stream
Future<void> _streamedBalance() async =>
_balancesSubject.add(await readBalances());

/// Get all token balances for accounts
/// key - address of account, value - list of balances for tokens in scope of
/// this account.
Future<Map<Address, List<AccountBalanceModel>>> readBalances() async {
final encoded = await _storage.getDomain(domain: _balancesDomain);

return encoded.map(
(key, value) {
final decoded = jsonDecode(value) as List<dynamic>;

return MapEntry(
Address(address: key),
decoded
.map(
(e) => AccountBalanceModel.fromJson(e as Map<String, dynamic>),
)
.toList(),
);
},
);
}

/// Set token balances for accounts with [accountAddress]
Future<void> setBalances({
required Address accountAddress,
required AccountBalanceModel balance,
}) async {
try {
var existedForAccount = balances[accountAddress];

if (existedForAccount == null) {
existedForAccount = [balance];
} else {
existedForAccount
..removeWhere((b) => b.rootTokenContract == balance.rootTokenContract)
..add(balance);
}

await _storage.set(
accountAddress.address,
jsonEncode(existedForAccount.map((b) => b.toJson()).toList()),
domain: _balancesDomain,
);
await _streamedOverallBalance();
} catch (e, t) {
_logger.severe('setBalances', e, t);
}
}

/// Delete token balances
Future<void> deleteBalances() async {
await _storage.clearDomain(_balancesDomain);
}
}
2 changes: 2 additions & 0 deletions lib/app/service/storage_service/storage_manager_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class StorageManagerService {
TonWalletStorageService tonWalletStorageService,
TokenWalletStorageService tokenWalletStorageService,
ConnectionsStorageService connectionStorageService,
BalanceStorageService balanceStorageService,
) : _storages = [
general,
browserTabs,
Expand All @@ -28,6 +29,7 @@ class StorageManagerService {
tonWalletStorageService,
tokenWalletStorageService,
connectionStorageService,
balanceStorageService,
];

final List<AbstractStorageService> _storages;
Expand Down
1 change: 1 addition & 0 deletions lib/app/service/storage_service/storage_service.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export 'abstract_storage_service.dart';
export 'account_seed_storage_service.dart';
export 'balance_storage_service.dart';
export 'browser_bookmarks_storage_service.dart';
export 'browser_favicon_url_storage_service.dart';
export 'browser_history_storage_service.dart';
Expand Down
27 changes: 27 additions & 0 deletions lib/data/models/account_balance.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'package:app/utils/utils.dart';
import 'package:collection/collection.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:nekoton_repository/nekoton_repository.dart';

part 'account_balance.freezed.dart';
part 'account_balance.g.dart';

/// Model of cached fiat and token balance that could be saved and restored
/// to access balance before creating subscription.
@immutable
@freezed
class AccountBalanceModel with _$AccountBalanceModel {
const factory AccountBalanceModel({
required Address rootTokenContract,
@moneyFromStringJsonConverter required Money fiatBalance,
@moneyFromStringJsonConverter required Money tokenBalance,
}) = _AccountBalanceModel;

factory AccountBalanceModel.fromJson(Map<String, dynamic> json) =>
_$AccountBalanceModelFromJson(json);
}

extension AccountBalancesExt on List<AccountBalanceModel> {
AccountBalanceModel? tokenBalance(Address rootTokenContract) =>
firstWhereOrNull((b) => b.rootTokenContract == rootTokenContract);
}
1 change: 1 addition & 0 deletions lib/data/models/models.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export 'account_balance.dart';
export 'account_interaction.dart';
export 'account_removed_event.dart';
export 'approval_request.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,11 @@ class _RequestPermissionsSheetState extends State<RequestPermissionsSheet> {
Widget _accountItem(KeyAccount account) {
return BlocProvider(
create: (_) => AccountCardCubit(
inject(),
account,
inject(),
inject(),
balanceStorage: inject(),
account: account,
currencyConvertService: inject(),
nekotonRepository: inject(),
balanceService: inject(),
),
child: BlocBuilder<AccountCardCubit, AccountCardState>(
builder: (context, state) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class TokenWalletAssetCubit extends Cubit<TokenWalletAssetState> {
required this.owner,
required this.currencyConvertService,
required this.nekotonRepository,
required this.balanceStorage,
}) : super(const TokenWalletAssetState.data()) {
_walletsSubscription =
nekotonRepository.tokenWalletsStream.listen((wallets) {
Expand All @@ -35,6 +36,7 @@ class TokenWalletAssetCubit extends Cubit<TokenWalletAssetState> {
_thisWalletSubscription = wallet.fieldUpdatesStream.listen((_) {
_cachedTokenBalance = wallet.moneyBalance;

_tryUpdateBalances();
_updateState();
});
_balanceSubscription = balanceService
Expand All @@ -45,12 +47,22 @@ class TokenWalletAssetCubit extends Cubit<TokenWalletAssetState> {
.listen((balance) {
_cachedFiatBalance = currencyConvertService.convert(balance);

_tryUpdateBalances();
_updateState();
});
}
});

final balances =
balanceStorage.balances[owner]?.tokenBalance(asset.address);
if (balances != null) {
_cachedFiatBalance = balances.fiatBalance;
_cachedTokenBalance = balances.tokenBalance;
_updateState();
}
}

final BalanceStorageService balanceStorage;
final TokenContractAsset asset;
final Address owner;
final BalanceService balanceService;
Expand Down Expand Up @@ -79,6 +91,19 @@ class TokenWalletAssetCubit extends Cubit<TokenWalletAssetState> {
_balanceSubscription?.cancel();
}

void _tryUpdateBalances() {
if (_cachedFiatBalance != null && _cachedTokenBalance != null) {
balanceStorage.setBalances(
accountAddress: owner,
balance: AccountBalanceModel(
rootTokenContract: asset.address,
fiatBalance: _cachedFiatBalance!,
tokenBalance: _cachedTokenBalance!,
),
);
}
}

void _updateState() {
emit(
TokenWalletAssetState.data(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class TokenWalletAssetWidget extends StatelessWidget {
balanceService: inject<BalanceService>(),
currencyConvertService: inject<CurrencyConvertService>(),
nekotonRepository: inject<NekotonRepository>(),
balanceStorage: inject(),
),
child: BlocBuilder<TokenWalletAssetCubit, TokenWalletAssetState>(
builder: (context, state) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import 'dart:async';

import 'package:app/app/service/service.dart';
import 'package:app/data/models/models.dart';
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:nekoton_repository/nekoton_repository.dart';

part 'ton_wallet_asset_state.dart';

part 'ton_wallet_asset_cubit.freezed.dart';
part 'ton_wallet_asset_state.dart';

class TonWalletAssetCubit extends Cubit<TonWalletAssetState> {
TonWalletAssetCubit({
required this.balanceService,
required this.tonWallet,
required this.currencyConvertService,
required this.nekotonRepository,
required this.balanceStorage,
}) : super(
TonWalletAssetState.data(
iconPath: nekotonRepository.currentTransport.nativeTokenIcon,
Expand All @@ -38,19 +39,30 @@ class TonWalletAssetCubit extends Cubit<TonWalletAssetState> {
Currencies()[nekotonRepository.currentTransport.nativeTokenTicker]!,
);

_tryUpdateBalances();
_updateState();
});
_balanceSubscription = balanceService
.tonWalletBalanceStream(tonWallet.address)
.listen((balance) {
_cachedFiatBalance = currencyConvertService.convert(balance);

_tryUpdateBalances();
_updateState();
});
}
});

final balances = balanceStorage.balances[tonWallet.address]
?.tokenBalance(_nativeTokenContract);
if (balances != null) {
_cachedFiatBalance = balances.fiatBalance;
_cachedTokenBalance = balances.tokenBalance;
_updateState();
}
}

final BalanceStorageService balanceStorage;
final TonWalletAsset tonWallet;
final BalanceService balanceService;
final CurrencyConvertService currencyConvertService;
Expand Down Expand Up @@ -78,6 +90,22 @@ class TonWalletAssetCubit extends Cubit<TonWalletAssetState> {
_balanceSubscription?.cancel();
}

Address get _nativeTokenContract =>
nekotonRepository.currentTransport.nativeTokenAddress;

void _tryUpdateBalances() {
if (_cachedFiatBalance != null && _cachedTokenBalance != null) {
balanceStorage.setBalances(
accountAddress: tonWallet.address,
balance: AccountBalanceModel(
rootTokenContract: _nativeTokenContract,
fiatBalance: _cachedFiatBalance!,
tokenBalance: _cachedTokenBalance!,
),
);
}
}

void _updateState() {
emit(
TonWalletAssetState.data(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class TonWalletAssetWidget extends StatelessWidget {
balanceService: inject<BalanceService>(),
currencyConvertService: inject<CurrencyConvertService>(),
nekotonRepository: inject<NekotonRepository>(),
balanceStorage: inject(),
),
child: BlocBuilder<TonWalletAssetCubit, TonWalletAssetState>(
builder: (context, state) {
Expand Down
Loading

0 comments on commit 24239a7

Please sign in to comment.