diff --git a/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart b/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart index 7501636e27..bfa2e15007 100644 --- a/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart +++ b/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart @@ -1,5 +1,8 @@ +import 'dart:async'; + import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart' show Asset; import 'package:komodo_defi_types/komodo_defi_types.dart' show NewAddressStatus; import 'package:web_dex/analytics/events.dart'; import 'package:web_dex/bloc/analytics/analytics_bloc.dart'; @@ -11,18 +14,27 @@ class CoinAddressesBloc extends Bloc { final KomodoDefiSdk sdk; final String assetId; final AnalyticsBloc analyticsBloc; - CoinAddressesBloc( - this.sdk, - this.assetId, - this.analyticsBloc, - ) : super(const CoinAddressesState()) { - on(_onSubmitCreateAddress); - on(_onLoadAddresses); - on(_onUpdateHideZeroBalance); + + StreamSubscription? _pubkeysSub; + CoinAddressesBloc(this.sdk, this.assetId, this.analyticsBloc) + : super(const CoinAddressesState()) { + on(_onCreateAddressSubmitted); + on(_onStarted); + on(_onAddressesSubscriptionRequested); + on(_onHideZeroBalanceChanged); + on(_onPubkeysUpdated); + on(_onPubkeysSubscriptionFailed); + } + + Future _onStarted( + CoinAddressesStarted event, + Emitter emit, + ) async { + add(const CoinAddressesSubscriptionRequested()); } - Future _onSubmitCreateAddress( - SubmitCreateAddressEvent event, + Future _onCreateAddressSubmitted( + CoinAddressesAddressCreationSubmitted event, Emitter emit, ) async { emit( @@ -52,7 +64,7 @@ class CoinAddressesBloc extends Bloc { ); } - add(const LoadAddressesEvent()); + add(const CoinAddressesSubscriptionRequested()); emit( state.copyWith( @@ -85,8 +97,8 @@ class CoinAddressesBloc extends Bloc { } } - Future _onLoadAddresses( - LoadAddressesEvent event, + Future _onAddressesSubscriptionRequested( + CoinAddressesSubscriptionRequested event, Emitter emit, ) async { emit(state.copyWith(status: () => FormStatus.submitting)); @@ -104,6 +116,8 @@ class CoinAddressesBloc extends Bloc { cantCreateNewAddressReasons: () => reasons, ), ); + + _startWatchingPubkeys(asset); } catch (e) { emit( state.copyWith( @@ -114,10 +128,63 @@ class CoinAddressesBloc extends Bloc { } } - void _onUpdateHideZeroBalance( - UpdateHideZeroBalanceEvent event, + void _onHideZeroBalanceChanged( + CoinAddressesZeroBalanceVisibilityChanged event, Emitter emit, ) { emit(state.copyWith(hideZeroBalance: () => event.hideZeroBalance)); } + + Future _onPubkeysUpdated( + CoinAddressesPubkeysUpdated event, + Emitter emit, + ) async { + try { + final asset = getSdkAsset(sdk, assetId); + final reasons = await asset.getCantCreateNewAddressReasons(sdk); + emit( + state.copyWith( + status: () => FormStatus.success, + addresses: () => event.addresses, + cantCreateNewAddressReasons: () => reasons, + errorMessage: () => null, + ), + ); + } catch (e) { + emit(state.copyWith(errorMessage: () => e.toString())); + } + } + + void _onPubkeysSubscriptionFailed( + CoinAddressesPubkeysSubscriptionFailed event, + Emitter emit, + ) { + emit(state.copyWith(errorMessage: () => event.error)); + } + + void _startWatchingPubkeys(Asset asset) { + _pubkeysSub?.cancel(); + // pre-cache pubkeys to ensure that any newly created pubkeys are available + // when we start watching. UI flickering between old and new states is + // avoided this way. The watchPubkeys function yields the last known pubkeys + // when the pubkeys stream is first activated. + sdk.pubkeys.preCachePubkeys(asset); + _pubkeysSub = sdk.pubkeys + .watchPubkeys(asset, activateIfNeeded: true) + .listen( + (assetPubkeys) { + add(CoinAddressesPubkeysUpdated(assetPubkeys.keys)); + }, + onError: (Object err) { + add(CoinAddressesPubkeysSubscriptionFailed(err.toString())); + }, + ); + } + + @override + Future close() async { + await _pubkeysSub?.cancel(); + _pubkeysSub = null; + return super.close(); + } } diff --git a/lib/bloc/coin_addresses/bloc/coin_addresses_event.dart b/lib/bloc/coin_addresses/bloc/coin_addresses_event.dart index 391396d142..3fa8979145 100644 --- a/lib/bloc/coin_addresses/bloc/coin_addresses_event.dart +++ b/lib/bloc/coin_addresses/bloc/coin_addresses_event.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart' show PubkeyInfo; abstract class CoinAddressesEvent extends Equatable { const CoinAddressesEvent(); @@ -7,19 +8,41 @@ abstract class CoinAddressesEvent extends Equatable { List get props => []; } -class SubmitCreateAddressEvent extends CoinAddressesEvent { - const SubmitCreateAddressEvent(); +class CoinAddressesAddressCreationSubmitted extends CoinAddressesEvent { + const CoinAddressesAddressCreationSubmitted(); } -class LoadAddressesEvent extends CoinAddressesEvent { - const LoadAddressesEvent(); +class CoinAddressesStarted extends CoinAddressesEvent { + const CoinAddressesStarted(); } -class UpdateHideZeroBalanceEvent extends CoinAddressesEvent { +class CoinAddressesSubscriptionRequested extends CoinAddressesEvent { + const CoinAddressesSubscriptionRequested(); +} + +class CoinAddressesZeroBalanceVisibilityChanged extends CoinAddressesEvent { final bool hideZeroBalance; - const UpdateHideZeroBalanceEvent(this.hideZeroBalance); + const CoinAddressesZeroBalanceVisibilityChanged(this.hideZeroBalance); @override List get props => [hideZeroBalance]; } + +/// Emitted when the pubkeys watcher emits an updated set of keys (and balances) +class CoinAddressesPubkeysUpdated extends CoinAddressesEvent { + final List addresses; + const CoinAddressesPubkeysUpdated(this.addresses); + + @override + List get props => [addresses]; +} + +/// Emitted when the pubkeys watcher reports an error +class CoinAddressesPubkeysSubscriptionFailed extends CoinAddressesEvent { + final String error; + const CoinAddressesPubkeysSubscriptionFailed(this.error); + + @override + List get props => [error]; +} diff --git a/lib/shared/widgets/coin_balance.dart b/lib/shared/widgets/coin_balance.dart index 3ffad1a5d4..04a52d34bf 100644 --- a/lib/shared/widgets/coin_balance.dart +++ b/lib/shared/widgets/coin_balance.dart @@ -7,11 +7,7 @@ import 'package:web_dex/shared/widgets/coin_fiat_balance.dart'; // TODO! Integrate this widget directly to the SDK and make it subscribe to // the balance changes of the coin. class CoinBalance extends StatelessWidget { - const CoinBalance({ - super.key, - required this.coin, - this.isVertical = false, - }); + const CoinBalance({super.key, required this.coin, this.isVertical = false}); final Coin coin; final bool isVertical; @@ -19,9 +15,7 @@ class CoinBalance extends StatelessWidget { @override Widget build(BuildContext context) { final baseFont = Theme.of(context).textTheme.bodySmall; - final balanceStyle = baseFont?.copyWith( - fontWeight: FontWeight.w500, - ); + final balanceStyle = baseFont?.copyWith(fontWeight: FontWeight.w500); final balance = context.sdk.balances.lastKnown(coin.id)?.spendable.toDouble() ?? 0.0; @@ -38,23 +32,15 @@ class CoinBalance extends StatelessWidget { textAlign: TextAlign.right, ), ), - Text( - ' ${Coin.normalizeAbbr(coin.abbr)}', - style: balanceStyle, - ), + Text(' ${Coin.normalizeAbbr(coin.abbr)}', style: balanceStyle), ], ), ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 100, - ), + constraints: const BoxConstraints(maxWidth: 100), child: Row( children: [ Text(' (', style: balanceStyle), - CoinFiatBalance( - coin, - isAutoScrollEnabled: true, - ), + CoinFiatBalance(coin, isAutoScrollEnabled: true), Text(')', style: balanceStyle), ], ), diff --git a/lib/shared/widgets/coin_fiat_balance.dart b/lib/shared/widgets/coin_fiat_balance.dart index f50e5c35e8..e771bb1cc2 100644 --- a/lib/shared/widgets/coin_fiat_balance.dart +++ b/lib/shared/widgets/coin_fiat_balance.dart @@ -23,31 +23,34 @@ class CoinFiatBalance extends StatelessWidget { Widget build(BuildContext context) { final balanceStream = context.sdk.balances.watchBalance(coin.id); - final TextStyle mergedStyle = - const TextStyle(fontSize: 12, fontWeight: FontWeight.w500).merge(style); + final TextStyle mergedStyle = const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + ).merge(style); return StreamBuilder( - stream: balanceStream, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const SizedBox(); - } - - final balanceStr = formatUsdValue( - coin.lastKnownUsdBalance(context.sdk), + stream: balanceStream, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const SizedBox(); + } + + final balanceStr = formatUsdValue( + coin.lastKnownUsdBalance(context.sdk), + ); + + if (isAutoScrollEnabled) { + return AutoScrollText( + text: balanceStr, + style: mergedStyle, + isSelectable: isSelectable, ); + } - if (isAutoScrollEnabled) { - return AutoScrollText( - text: balanceStr, - style: mergedStyle, - isSelectable: isSelectable, - ); - } - - return isSelectable - ? SelectableText(balanceStr, style: mergedStyle) - : Text(balanceStr, style: mergedStyle); - }); + return isSelectable + ? SelectableText(balanceStr, style: mergedStyle) + : Text(balanceStr, style: mergedStyle); + }, + ); } } diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_addresses.dart b/lib/views/wallet/coin_details/coin_details_info/coin_addresses.dart index 2a56e5b724..2eaf716e85 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_addresses.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_addresses.dart @@ -13,6 +13,7 @@ import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_state.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; +import 'package:web_dex/shared/utils/formatters.dart'; import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/shared/widgets/coin_type_tag.dart'; import 'package:web_dex/shared/widgets/truncate_middle_text.dart'; @@ -243,7 +244,6 @@ class AddressCard extends StatelessWidget { ), const SizedBox(height: 12), _Balance(address: address, coin: coin), - const SizedBox(height: 4), ], ) : SizedBox( @@ -286,9 +286,13 @@ class _Balance extends StatelessWidget { @override Widget build(BuildContext context) { + final balance = address.balance.total.toDouble(); + final price = coin.lastKnownUsdPrice(context.sdk); + final usdValue = price == null ? null : price * balance; + final fiat = formatUsdValue(usdValue); + return Text( - '${doubleToString(address.balance.total.toDouble())} ' - '${abbr2Ticker(coin.abbr)} (${address.balance.total.toDouble()})', + '${doubleToString(balance)} ${abbr2Ticker(coin.abbr)} ($fiat)', style: TextStyle(fontSize: isMobile ? 12 : 14), ); } @@ -642,7 +646,7 @@ class HideZeroBalanceCheckbox extends StatelessWidget { value: hideZeroBalance, onChanged: (value) { context.read().add( - UpdateHideZeroBalanceEvent(value), + CoinAddressesZeroBalanceVisibilityChanged(value), ); }, ); @@ -683,7 +687,7 @@ class CreateButton extends StatelessWidget { createAddressStatus != FormStatus.submitting ? () { context.read().add( - const SubmitCreateAddressEvent(), + const CoinAddressesAddressCreationSubmitted(), ); } : null, diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart b/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart index 10d3a6dca4..067a0dd5ee 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart @@ -65,7 +65,7 @@ class _CoinDetailsInfoState extends State context.sdk, widget.coin.abbr, context.read(), - )..add(LoadAddressesEvent()); + )..add(const CoinAddressesStarted()); @override void initState() { @@ -73,22 +73,22 @@ class _CoinDetailsInfoState extends State const selectedDurationInitial = Duration(hours: 1); context.read().add( - PortfolioGrowthLoadRequested( - coins: [widget.coin], - fiatCoinId: 'USDT', - selectedPeriod: selectedDurationInitial, - walletId: _walletId!, - ), - ); + PortfolioGrowthLoadRequested( + coins: [widget.coin], + fiatCoinId: 'USDT', + selectedPeriod: selectedDurationInitial, + walletId: _walletId!, + ), + ); context.read().add( - ProfitLossPortfolioChartLoadRequested( - coins: [widget.coin], - selectedPeriod: const Duration(hours: 1), - fiatCoinId: 'USDT', - walletId: _walletId!, - ), - ); + ProfitLossPortfolioChartLoadRequested( + coins: [widget.coin], + selectedPeriod: const Duration(hours: 1), + fiatCoinId: 'USDT', + walletId: _walletId!, + ), + ); } @override @@ -100,9 +100,9 @@ class _CoinDetailsInfoState extends State previous.createAddressStatus != current.createAddressStatus && current.createAddressStatus == FormStatus.success, listener: (context, state) { - context - .read() - .add(CoinsPubkeysRequested(widget.coin.abbr)); + context.read().add( + CoinsPubkeysRequested(widget.coin.abbr), + ); }, child: PageLayout( padding: const EdgeInsets.fromLTRB(15, 32, 15, 20), @@ -118,9 +118,7 @@ class _CoinDetailsInfoState extends State onBackButtonPressed: _onBackButtonPressed, actions: [_buildDisableButton()], ), - content: Expanded( - child: _buildContent(context), - ), + content: Expanded(child: _buildContent(context)), ), ), ); @@ -148,9 +146,13 @@ class _CoinDetailsInfoState extends State return DisableCoinButton( onClick: () { - confirmBeforeDisablingCoin(widget.coin, context, onConfirm: () { - widget.onBackButtonPressed(); - }); + confirmBeforeDisablingCoin( + widget.coin, + context, + onConfirm: () { + widget.onBackButtonPressed(); + }, + ); }, ); } @@ -209,19 +211,12 @@ class _DesktopContent extends StatelessWidget { slivers: [ if (selectedTransaction == null) SliverToBoxAdapter( - child: _DesktopCoinDetails( - coin: coin, - setPageType: setPageType, - ), + child: _DesktopCoinDetails(coin: coin, setPageType: setPageType), ), - const SliverToBoxAdapter( - child: SizedBox(height: 20), - ), + const SliverToBoxAdapter(child: SizedBox(height: 20)), if (selectedTransaction == null) CoinAddresses(coin: coin, setPageType: setPageType), - const SliverToBoxAdapter( - child: SizedBox(height: 20), - ), + const SliverToBoxAdapter(child: SizedBox(height: 20)), TransactionTable( coin: coin, selectedTransaction: selectedTransaction, @@ -234,10 +229,7 @@ class _DesktopContent extends StatelessWidget { } class _DesktopCoinDetails extends StatelessWidget { - const _DesktopCoinDetails({ - required this.coin, - required this.setPageType, - }); + const _DesktopCoinDetails({required this.coin, required this.setPageType}); final Coin coin; final void Function(CoinPageType) setPageType; @@ -254,25 +246,16 @@ class _DesktopCoinDetails extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.fromLTRB(0, 5, 12, 0), - child: AssetLogo.ofId( - coin.id, - size: 50, - ), + child: AssetLogo.ofId(coin.id, size: 50), ), _Balance(coin: coin), const SizedBox(width: 10), Padding( padding: const EdgeInsets.only(top: 18.0), - child: _SpecificButton( - coin: coin, - selectWidget: setPageType, - ), + child: _SpecificButton(coin: coin, selectWidget: setPageType), ), const Spacer(), - CoinDetailsInfoFiat( - coin: coin, - isMobile: false, - ), + CoinDetailsInfoFiat(coin: coin, isMobile: false), ], ), Padding( @@ -282,8 +265,8 @@ class _DesktopCoinDetails extends StatelessWidget { selectWidget: setPageType, onClickSwapButton: context.watch().state is TradingEnabled - ? () => _goToSwap(context, coin) - : null, + ? () => _goToSwap(context, coin) + : null, coin: coin, ), ), @@ -320,17 +303,10 @@ class _MobileContent extends StatelessWidget { context: context, ), ), - const SliverToBoxAdapter( - child: SizedBox(height: 20), - ), + const SliverToBoxAdapter(child: SizedBox(height: 20)), if (selectedTransaction == null) - CoinAddresses( - coin: coin, - setPageType: setPageType, - ), - const SliverToBoxAdapter( - child: SizedBox(height: 20), - ), + CoinAddresses(coin: coin, setPageType: setPageType), + const SliverToBoxAdapter(child: SizedBox(height: 20)), TransactionTable( coin: coin, selectedTransaction: selectedTransaction, @@ -362,20 +338,14 @@ class _CoinDetailsInfoHeader extends StatelessWidget { ), child: Column( children: [ - AssetIcon.ofTicker( - coin.abbr, - size: 35, - ), + AssetIcon.ofTicker(coin.abbr, size: 35), const SizedBox(height: 8), _Balance(coin: coin), const SizedBox(height: 12), _SpecificButton(coin: coin, selectWidget: setPageType), Padding( padding: const EdgeInsets.only(top: 15.0), - child: CoinDetailsInfoFiat( - coin: coin, - isMobile: true, - ), + child: CoinDetailsInfoFiat(coin: coin, isMobile: true), ), Padding( padding: const EdgeInsets.only(top: 12.0, bottom: 14.0), @@ -384,8 +354,8 @@ class _CoinDetailsInfoHeader extends StatelessWidget { selectWidget: setPageType, onClickSwapButton: context.watch().state is TradingEnabled - ? () => _goToSwap(context, coin) - : null, + ? () => _goToSwap(context, coin) + : null, coin: coin, ), ), @@ -432,23 +402,23 @@ class _CoinDetailsMarketMetricsTabBarState if (growthState is PortfolioGrowthChartLoadSuccess) { final period = _formatDuration(growthState.selectedPeriod); context.read().logEvent( - PortfolioGrowthViewedEventData( - period: period, - growthPct: growthState.percentageIncrease, - ), - ); + PortfolioGrowthViewedEventData( + period: period, + growthPct: growthState.percentageIncrease, + ), + ); } } else if (_tabController!.index == 1) { final profitLossState = context.read().state; if (profitLossState is PortfolioProfitLossChartLoadSuccess) { final timeframe = _formatDuration(profitLossState.selectedPeriod); context.read().logEvent( - PortfolioPnlViewedEventData( - timeframe: timeframe, - realizedPnl: profitLossState.totalValue, - unrealizedPnl: 0, - ), - ); + PortfolioPnlViewedEventData( + timeframe: timeframe, + realizedPnl: profitLossState.totalValue, + unrealizedPnl: 0, + ), + ); } } } @@ -566,8 +536,9 @@ class _Balance extends StatelessWidget { final value = balance == null ? null : doubleToString(balance); return Column( - crossAxisAlignment: - isMobile ? CrossAxisAlignment.center : CrossAxisAlignment.start, + crossAxisAlignment: isMobile + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ if (isMobile) @@ -584,8 +555,9 @@ class _Balance extends StatelessWidget { Flexible( child: Row( mainAxisSize: isMobile ? MainAxisSize.max : MainAxisSize.min, - mainAxisAlignment: - isMobile ? MainAxisAlignment.center : MainAxisAlignment.start, + mainAxisAlignment: isMobile + ? MainAxisAlignment.center + : MainAxisAlignment.start, children: [ Flexible( child: AutoScrollText( @@ -632,9 +604,9 @@ class _FiatBalance extends StatelessWidget { Text( LocaleKeys.fiatBalance.tr(), style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontSize: 14, - fontWeight: FontWeight.w500, - ), + fontSize: 14, + fontWeight: FontWeight.w500, + ), ), Padding( padding: const EdgeInsets.only(left: 6.0),