Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/bloc/app_bloc_root.dart
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ class AppBlocRoot extends StatelessWidget {
komodoDefiSdk,
)..add(
const PriceChartStarted(
symbols: ['KMD'],
symbols: ['BTC'],
period: Duration(days: 30),
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:web_dex/bloc/cex_market_data/sdk_auth_activation_extension.dart'
import 'package:web_dex/mm2/mm2_api/rpc/base.dart';
import 'package:web_dex/model/coin.dart';
import 'package:web_dex/model/text_error.dart';
import 'package:web_dex/shared/utils/extensions/legacy_coin_migration_extensions.dart';

part 'portfolio_growth_event.dart';
part 'portfolio_growth_state.dart';
Expand Down Expand Up @@ -120,10 +121,13 @@ class PortfolioGrowthBloc
await emit.forEach(
// computation is omitted, so null-valued events are emitted on a set
// interval.
Stream<Object?>.periodic(event.updateFrequency)
.asyncMap((_) async => _fetchPortfolioGrowthChart(event)),
Stream<Object?>.periodic(event.updateFrequency).asyncMap((_) async {
// Update prices before fetching chart data
await portfolioGrowthRepository.updatePrices();
return _fetchPortfolioGrowthChart(event);
}),
onData: (data) =>
_handlePortfolioGrowthUpdate(data, event.selectedPeriod),
_handlePortfolioGrowthUpdate(data, event.selectedPeriod, event.coins),
onError: (error, stackTrace) {
_log.shout('Failed to load portfolio growth', error, stackTrace);
return GrowthChartLoadFailure(
Expand Down Expand Up @@ -164,10 +168,21 @@ class PortfolioGrowthBloc
return state;
}

// Fetch prices before calculating total change
// This ensures we have the latest prices in the cache
await portfolioGrowthRepository.updatePrices();

final totalBalance = _calculateTotalBalance(coins);
final totalChange24h = _calculateTotalChange24h(coins);
final percentageChange24h = _calculatePercentageChange24h(coins);

return PortfolioGrowthChartLoadSuccess(
portfolioGrowth: chart,
percentageIncrease: chart.percentageIncrease,
selectedPeriod: event.selectedPeriod,
totalBalance: totalBalance,
totalChange24h: totalChange24h,
percentageChange24h: percentageChange24h,
);
}

Expand Down Expand Up @@ -205,20 +220,71 @@ class PortfolioGrowthBloc
PortfolioGrowthState _handlePortfolioGrowthUpdate(
ChartData growthChart,
Duration selectedPeriod,
List<Coin> coins,
) {
if (growthChart.isEmpty && state is PortfolioGrowthChartLoadSuccess) {
return state;
}

final percentageIncrease = growthChart.percentageIncrease;

// TODO? Include the center value in the bloc state instead of
// calculating it in the UI
final totalBalance = _calculateTotalBalance(coins);
final totalChange24h = _calculateTotalChange24h(coins);
final percentageChange24h = _calculatePercentageChange24h(coins);

return PortfolioGrowthChartLoadSuccess(
portfolioGrowth: growthChart,
percentageIncrease: percentageIncrease,
selectedPeriod: selectedPeriod,
totalBalance: totalBalance,
totalChange24h: totalChange24h,
percentageChange24h: percentageChange24h,
);
}

/// Calculate the total balance of all coins in USD
double _calculateTotalBalance(List<Coin> coins) {
double total = coins.fold(
0,
(prev, coin) => prev + (coin.lastKnownUsdBalance(sdk) ?? 0),
);

// Return at least 0.01 if total is positive but very small
if (total > 0 && total < 0.01) {
return 0.01;
}

return total;
}

/// Calculate the total 24h change in USD value
double _calculateTotalChange24h(List<Coin> coins) {
// Calculate the 24h change by summing the change percentage of each coin
// multiplied by its USD balance and divided by 100 (to convert percentage to decimal)
return coins.fold(
0.0,
(sum, coin) {
// Use the price change from the CexPrice if available
final usdBalance = coin.lastKnownUsdBalance(sdk) ?? 0.0;
// Get the coin price from the repository's prices cache
final price = portfolioGrowthRepository
.getCachedPrice(coin.id.symbol.configSymbol.toUpperCase());
final change24h = price?.change24h ?? 0.0;
return sum + (change24h * usdBalance / 100);
},
);
}

/// Calculate the percentage change over 24h for the entire portfolio
double _calculatePercentageChange24h(List<Coin> coins) {
final double totalBalance = _calculateTotalBalance(coins);
final double totalChange = _calculateTotalChange24h(coins);

// Avoid division by zero or very small balances
if (totalBalance <= 0.01) {
return 0.0;
}

// Return the percentage change
return (totalChange / totalBalance) * 100;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,12 @@ class PortfolioGrowthRepository {
/// The graph cache provider to store the portfolio growth graph data.
final PersistenceProvider<String, GraphCache> _graphCache;

final CoinsRepo _coinsRepository;
/// The SDK needed for connecting to blockchain nodes
final KomodoDefiSdk _sdk;

/// The coins repository for detailed coin info
final CoinsRepo _coinsRepository;

final _log = Logger('PortfolioGrowthRepository');

static Future<void> ensureInitialized() async {
Expand Down Expand Up @@ -514,4 +517,38 @@ class PortfolioGrowthRepository {
}

Future<void> clearCache() => _graphCache.deleteAll();

/// Calculate the total 24h change in USD value for a list of coins
///
/// This method fetches the current prices for all coins and calculates
/// the 24h change by multiplying each coin's percentage change by its USD balance
Future<double> calculateTotalChange24h(List<Coin> coins) async {
// Fetch current prices including 24h change data
final prices = await _coinsRepository.fetchCurrentPrices() ?? {};

// Calculate the 24h change by summing the change percentage of each coin
// multiplied by its USD balance and divided by 100 (to convert percentage to decimal)
double totalChange = 0.0;
for (final coin in coins) {
final price = prices[coin.id.symbol.configSymbol.toUpperCase()];
final change24h = price?.change24h ?? 0.0;
final usdBalance = coin.lastKnownUsdBalance(_sdk) ?? 0.0;
totalChange += (change24h * usdBalance / 100);
}
return totalChange;
}

/// Get the cached price for a given coin symbol
///
/// This is used to avoid fetching prices for every calculation
CexPrice? getCachedPrice(String symbol) {
return _coinsRepository.getCachedPrice(symbol);
}

/// Update prices for all coins by fetching from market data
///
/// This method ensures we have up-to-date price data before calculations
Future<void> updatePrices() async {
await _coinsRepository.fetchCurrentPrices();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,25 @@ final class PortfolioGrowthChartLoadSuccess extends PortfolioGrowthState {
required this.portfolioGrowth,
required this.percentageIncrease,
required super.selectedPeriod,
required this.totalBalance,
required this.totalChange24h,
required this.percentageChange24h,
});

final ChartData portfolioGrowth;
final double percentageIncrease;
final double totalBalance;
final double totalChange24h;
final double percentageChange24h;

@override
List<Object> get props => <Object>[
portfolioGrowth,
percentageIncrease,
selectedPeriod,
totalBalance,
totalChange24h,
percentageChange24h,
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ enum PriceChartPeriod {
}
}

String get intervalString {
// TODO: Localize this
String get formatted {
switch (this) {
case PriceChartPeriod.oneHour:
return '1h';
Expand Down
3 changes: 3 additions & 0 deletions lib/bloc/cex_market_data/price_chart/models/time_period.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ enum TimePeriod {
}
}

// TODO: Localize
String formatted() => name;

Duration get duration {
switch (this) {
case TimePeriod.oneHour:
Expand Down
7 changes: 7 additions & 0 deletions lib/bloc/coins_bloc/coins_repo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -658,4 +658,11 @@ class CoinsRepo {
result: withdrawDetails,
);
}

/// Get a cached price for a given coin symbol
///
/// This returns the price from the cache without fetching new data
CexPrice? getCachedPrice(String symbol) {
return _pricesCache[symbol];
}
}
78 changes: 4 additions & 74 deletions lib/model/cex_price.dart
Original file line number Diff line number Diff line change
@@ -1,82 +1,12 @@
import 'package:equatable/equatable.dart';
import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'
as sdk_types;

enum CexDataProvider {
binance,
coingecko,
coinpaprika,
nomics,
unknown,
}
typedef CexDataProvider = sdk_types.CexDataProvider;

CexDataProvider cexDataProvider(String string) {
return CexDataProvider.values.firstWhere(
(e) => e.toString().split('.').last == string,
orElse: () => CexDataProvider.unknown);
}

class CexPrice extends Equatable {
const CexPrice({
required this.ticker,
required this.price,
this.lastUpdated,
this.priceProvider,
this.change24h,
this.changeProvider,
this.volume24h,
this.volumeProvider,
});

final String ticker;
final double price;
final DateTime? lastUpdated;
final CexDataProvider? priceProvider;
final double? volume24h;
final CexDataProvider? volumeProvider;
final double? change24h;
final CexDataProvider? changeProvider;

@override
String toString() {
return 'CexPrice(ticker: $ticker, price: $price)';
}

factory CexPrice.fromJson(Map<String, dynamic> json) {
return CexPrice(
ticker: json['ticker'] as String,
price: (json['price'] as num).toDouble(),
lastUpdated: json['lastUpdated'] == null
? null
: DateTime.parse(json['lastUpdated'] as String),
priceProvider: cexDataProvider(json['priceProvider'] as String),
volume24h: (json['volume24h'] as num?)?.toDouble(),
volumeProvider: cexDataProvider(json['volumeProvider'] as String),
change24h: (json['change24h'] as num?)?.toDouble(),
changeProvider: cexDataProvider(json['changeProvider'] as String),
);
}

Map<String, dynamic> toJson() {
return {
'ticker': ticker,
'price': price,
'lastUpdated': lastUpdated?.toIso8601String(),
'priceProvider': priceProvider?.toString(),
'volume24h': volume24h,
'volumeProvider': volumeProvider?.toString(),
'change24h': change24h,
'changeProvider': changeProvider?.toString(),
};
}

@override
List<Object?> get props => [
ticker,
price,
lastUpdated,
priceProvider,
volume24h,
volumeProvider,
change24h,
changeProvider,
];
}
typedef CexPrice = sdk_types.CexPrice;
2 changes: 1 addition & 1 deletion lib/shared/widgets/coin_balance.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class CoinBalance extends StatelessWidget {
),
child: Row(
children: [
Text('(', style: balanceStyle),
Text(' (', style: balanceStyle),
CoinFiatBalance(
coin,
isAutoScrollEnabled: true,
Expand Down
2 changes: 2 additions & 0 deletions lib/views/wallet/wallet_page/common/asset_list_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ class AssetListItem extends StatelessWidget {
required this.backgroundColor,
required this.onTap,
this.isActivating = false,
this.priceChangePercentage24h,
});

final AssetId assetId;
final Color backgroundColor;
final void Function(AssetId) onTap;
final bool isActivating;
final double? priceChangePercentage24h;

@override
Widget build(BuildContext context) {
Expand Down
1 change: 1 addition & 0 deletions lib/views/wallet/wallet_page/common/assets_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class AssetsList extends StatelessWidget {
assetId: asset,
backgroundColor: backgroundColor,
onTap: onAssetItemTap,
priceChangePercentage24h: priceChangePercentages[asset.id],
);
},
childCount: filteredAssets.length,
Expand Down
Loading
Loading