From 8e6f3a160fdd4be0b93ae3c65d76cae1ae1fd5b3 Mon Sep 17 00:00:00 2001 From: Francois Date: Fri, 6 Sep 2024 16:05:38 +0200 Subject: [PATCH 1/3] Fetch coins list on refresh for portfolio growth chart --- .../portfolio_growth_bloc.dart | 27 ++++++++++++------- .../portfolio_growth_repository.dart | 23 +++++++++------- .../profit_loss/profit_loss_bloc.dart | 25 ++++++++++------- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart b/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart index c02a699ca1..d3ad938e90 100644 --- a/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart +++ b/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart @@ -61,15 +61,10 @@ class PortfolioGrowthBloc PortfolioGrowthLoadRequested event, Emitter emit, ) async { - final List coins = List.from(event.coins); - await coins.removeWhereAsync( - (Coin coin) async { - final isCoinSupported = await portfolioGrowthRepository - .isCoinChartSupported(coin, event.fiatCoinId); - return !isCoinSupported; - }, - ); - if (coins.isEmpty) { + List coins = await _removeUnsupportedCoins(event); + // Charts for individual coins (coin details) are parsed here as well, + // and should be hidden if not supported. + if (coins.isEmpty && event.coins.length <= 1) { return emit( PortfolioGrowthChartUnsupported(selectedPeriod: event.selectedPeriod), ); @@ -128,6 +123,7 @@ class PortfolioGrowthBloc await emit.forEach( Stream.periodic(event.updateFrequency).asyncMap((_) async { // Do not let transaction loading exceptions stop the periodic updates + coins = await _removeUnsupportedCoins(event); try { return await portfolioGrowthRepository.getPortfolioGrowthChart( coins, @@ -171,4 +167,17 @@ class PortfolioGrowthBloc ); } } + + Future> _removeUnsupportedCoins( + PortfolioGrowthLoadRequested event) async { + final List coins = List.from(event.coins); + await coins.removeWhereAsync( + (Coin coin) async { + final isCoinSupported = await portfolioGrowthRepository + .isCoinChartSupported(coin.abbr, event.fiatCoinId); + return !isCoinSupported; + }, + ); + return coins; + } } diff --git a/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_repository.dart b/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_repository.dart index c748ee84e5..6fe662065c 100644 --- a/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_repository.dart +++ b/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_repository.dart @@ -10,6 +10,7 @@ import 'package:web_dex/bloc/cex_market_data/mockup/performance_mode.dart'; import 'package:web_dex/bloc/cex_market_data/models/graph_type.dart'; import 'package:web_dex/bloc/cex_market_data/models/models.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_repo.dart'; +import 'package:web_dex/blocs/blocs.dart'; import 'package:web_dex/mm2/mm2_api/rpc/my_tx_history/transaction.dart'; import 'package:web_dex/model/coin.dart'; @@ -81,7 +82,7 @@ class PortfolioGrowthRepository { /// /// Returns the growth [ChartData] for the coin ([List] of [Point]). Future getCoinGrowthChart( - Coin coin, { + String coinId, { // avoid the possibility of accidentally swapping the order of these // required parameters by using named parameters required String fiatCoinId, @@ -92,7 +93,7 @@ class PortfolioGrowthRepository { }) async { if (useCache) { final String compoundKey = GraphCache.getPrimaryKey( - coin.abbr, + coinId, fiatCoinId, GraphType.balanceGrowth, walletId, @@ -106,6 +107,9 @@ class PortfolioGrowthRepository { } } + // TODO: Refactor referenced coinsBloc method to a repository. + // NB: Even though the class is called [CoinsBloc], it is not a Bloc. + final Coin coin = coinsBlocRepository.getCoin(coinId)!; final List transactions = await _transactionHistoryRepository.fetchCompletedTransactions(coin); @@ -115,7 +119,7 @@ class PortfolioGrowthRepository { // called later with useCache set to false to fetch the transactions again await _graphCache.insert( GraphCache( - coinId: coin.abbr, + coinId: coinId, fiatCoinId: fiatCoinId, lastUpdated: DateTime.now(), graph: List.empty(), @@ -180,7 +184,7 @@ class PortfolioGrowthRepository { /// [walletId] is the wallet id of the portfolio. /// [useCache] is a flag to indicate whether to use the cache. /// [startAt] and [endAt] will filter the final chart to the specified range, - /// and cache the filtered chart. + /// and cache the filtered chart. /// [ignoreTransactionFetchErrors] is a flag to ignore transaction fetch errors /// and return an empty chart instead. /// @@ -208,7 +212,7 @@ class PortfolioGrowthRepository { final chartDataFutures = coins.map((coin) async { try { return await getCoinGrowthChart( - coin, + coin.abbr, fiatCoinId: fiatCoinId, useCache: useCache, walletId: walletId, @@ -280,24 +284,25 @@ class PortfolioGrowthRepository { /// Returns `true` if the coin is supported by the CEX API for charting. /// Returns `false` if the coin is not supported by the CEX API for charting. Future isCoinChartSupported( - Coin coin, + String coinId, String fiatCoinId, { bool allowFiatAsBase = true, }) async { + final Coin coin = coinsBlocRepository.getCoin(coinId)!; if (coin.isActivating || !coin.isActive) { return false; } final supportedCoins = await _cexRepository.getCoinList(); - final coinId = coin.abbr.split('-').firstOrNull?.toUpperCase() ?? ''; + final coinTicker = coin.abbr.split('-').firstOrNull?.toUpperCase() ?? ''; // Allow fiat coins through, as they are represented by a constant value, // 1, in the repository layer and are not supported by the CEX API - if (allowFiatAsBase && coinId == fiatCoinId.toUpperCase()) { + if (allowFiatAsBase && coinTicker == fiatCoinId.toUpperCase()) { return true; } final coinPair = CexCoinPair( - baseCoinTicker: coinId, + baseCoinTicker: coinTicker, relCoinTicker: fiatCoinId.toUpperCase(), ); final isCoinSupported = coinPair.isCoinSupported(supportedCoins); diff --git a/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart b/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart index 3bde7547c4..20d865efa3 100644 --- a/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart +++ b/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart @@ -38,15 +38,10 @@ class ProfitLossBloc extends Bloc { Emitter emit, ) async { try { - final List coins = List.from(event.coins); - await coins.removeWhereAsync( - (Coin coin) async { - final isCoinSupported = await _profitLossRepository - .isCoinChartSupported(coin.abbr, event.fiatCoinId); - return coin.isTestCoin || !isCoinSupported; - }, - ); - if (coins.isEmpty) { + final List coins = await _removeUnsupportedCons(event); + // Charts for individual coins (coin details) are parsed here as well, + // and should be hidden if not supported. + if (coins.isEmpty && event.coins.length <= 1) { return emit( PortfolioProfitLossChartUnsupported( selectedPeriod: event.selectedPeriod, @@ -102,6 +97,18 @@ class ProfitLossBloc extends Bloc { } } + Future> _removeUnsupportedCons(ProfitLossPortfolioChartLoadRequested event) async { + final List coins = List.from(event.coins); + await coins.removeWhereAsync( + (Coin coin) async { + final isCoinSupported = await _profitLossRepository + .isCoinChartSupported(coin.abbr, event.fiatCoinId); + return coin.isTestCoin || !isCoinSupported; + }, + ); + return coins; + } + Future _onPeriodChanged( ProfitLossPeriodChanged event, Emitter emit, From 7c44bb217722b2c3ba70287b7ab2a9bc608b4f06 Mon Sep 17 00:00:00 2001 From: Francois Date: Fri, 6 Sep 2024 16:09:35 +0200 Subject: [PATCH 2/3] Remove unused profit/loss event and handler --- .../profit_loss/profit_loss_bloc.dart | 20 ------------------- .../profit_loss/profit_loss_event.dart | 11 ---------- 2 files changed, 31 deletions(-) diff --git a/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart b/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart index 20d865efa3..e39c478abe 100644 --- a/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart +++ b/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart @@ -27,7 +27,6 @@ class ProfitLossBloc extends Bloc { transformer: restartable(), ); - on(_onPeriodChanged); on(_onPortfolioPeriodChanged); } @@ -109,25 +108,6 @@ class ProfitLossBloc extends Bloc { return coins; } - Future _onPeriodChanged( - ProfitLossPeriodChanged event, - Emitter emit, - ) async { - if (state is! PortfolioProfitLossChartLoadSuccess) { - return; - } - - final currentState = state as PortfolioProfitLossChartLoadSuccess; - add( - ProfitLossPortfolioChartLoadRequested( - coins: currentState.coins, - fiatCoinId: currentState.fiatCurrency, - selectedPeriod: event.selectedPeriod, - walletId: currentState.walletId, - ), - ); - } - Future _onPortfolioPeriodChanged( ProfitLossPortfolioPeriodChanged event, Emitter emit, diff --git a/lib/bloc/cex_market_data/profit_loss/profit_loss_event.dart b/lib/bloc/cex_market_data/profit_loss/profit_loss_event.dart index 5452c7108e..20cac5f99d 100644 --- a/lib/bloc/cex_market_data/profit_loss/profit_loss_event.dart +++ b/lib/bloc/cex_market_data/profit_loss/profit_loss_event.dart @@ -24,17 +24,6 @@ class ProfitLossPortfolioChartLoadRequested extends ProfitLossEvent { List get props => [coins, fiatCoinId, selectedPeriod, walletId]; } -class ProfitLossPeriodChanged extends ProfitLossEvent { - const ProfitLossPeriodChanged({ - required this.selectedPeriod, - }); - - final Duration selectedPeriod; - - @override - List get props => [selectedPeriod]; -} - class ProfitLossPortfolioPeriodChanged extends ProfitLossEvent { const ProfitLossPortfolioPeriodChanged({ required this.selectedPeriod, From be47e00f6116d401a7263bdf6d5ee920ff47798d Mon Sep 17 00:00:00 2001 From: Francois Date: Fri, 6 Sep 2024 16:28:16 +0200 Subject: [PATCH 3/3] Add periodic refresh to the profit/loss chart Profit/loss chart would previously only refresh from an error state if the user interacted with the period selection dropdown --- .../profit_loss/profit_loss_bloc.dart | 37 +++++++++++++++++-- .../profit_loss/profit_loss_event.dart | 14 ++++++- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart b/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart index e39c478abe..fe2daafd8c 100644 --- a/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart +++ b/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart @@ -38,8 +38,8 @@ class ProfitLossBloc extends Bloc { ) async { try { final List coins = await _removeUnsupportedCons(event); - // Charts for individual coins (coin details) are parsed here as well, - // and should be hidden if not supported. + // Charts for individual coins (coin details) are parsed here as well, + // and should be hidden if not supported. if (coins.isEmpty && event.coins.length <= 1) { return emit( PortfolioProfitLossChartUnsupported( @@ -94,9 +94,40 @@ class ProfitLossBloc extends Bloc { ), ); } + + await emit.forEach( + Stream.periodic(event.updateFrequency).asyncMap((_) async { + return await _getSortedProfitLossChartForCoins( + event, + useCache: false, + ); + }), onData: (profitLossChart) { + if (profitLossChart.isEmpty) { + return state; + } + + final unCachedProfitIncrease = profitLossChart.increase; + final unCachedPercentageIncrease = profitLossChart.percentageIncrease; + return PortfolioProfitLossChartLoadSuccess( + profitLossChart: profitLossChart, + totalValue: unCachedProfitIncrease, + percentageIncrease: unCachedPercentageIncrease, + coins: event.coins, + fiatCurrency: event.fiatCoinId, + selectedPeriod: event.selectedPeriod, + walletId: event.walletId, + ); + }, onError: (e, s) { + logger.log('Failed to load portfolio profit/loss: $e', isError: true); + return ProfitLossLoadFailure( + error: TextError(error: 'Failed to load portfolio profit/loss: $e'), + selectedPeriod: event.selectedPeriod, + ); + }); } - Future> _removeUnsupportedCons(ProfitLossPortfolioChartLoadRequested event) async { + Future> _removeUnsupportedCons( + ProfitLossPortfolioChartLoadRequested event) async { final List coins = List.from(event.coins); await coins.removeWhereAsync( (Coin coin) async { diff --git a/lib/bloc/cex_market_data/profit_loss/profit_loss_event.dart b/lib/bloc/cex_market_data/profit_loss/profit_loss_event.dart index 20cac5f99d..76c6f6ff89 100644 --- a/lib/bloc/cex_market_data/profit_loss/profit_loss_event.dart +++ b/lib/bloc/cex_market_data/profit_loss/profit_loss_event.dart @@ -13,24 +13,34 @@ class ProfitLossPortfolioChartLoadRequested extends ProfitLossEvent { required this.fiatCoinId, required this.selectedPeriod, required this.walletId, + this.updateFrequency = const Duration(minutes: 1), }); final List coins; final String fiatCoinId; final Duration selectedPeriod; + final Duration updateFrequency; final String walletId; @override - List get props => [coins, fiatCoinId, selectedPeriod, walletId]; + List get props => [ + coins, + fiatCoinId, + selectedPeriod, + walletId, + updateFrequency, + ]; } class ProfitLossPortfolioPeriodChanged extends ProfitLossEvent { const ProfitLossPortfolioPeriodChanged({ required this.selectedPeriod, + this.updateFrequency = const Duration(minutes: 1), }); final Duration selectedPeriod; + final Duration updateFrequency; @override - List get props => [selectedPeriod]; + List get props => [selectedPeriod, updateFrequency]; }