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
97 changes: 82 additions & 15 deletions lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -11,18 +14,27 @@ class CoinAddressesBloc extends Bloc<CoinAddressesEvent, CoinAddressesState> {
final KomodoDefiSdk sdk;
final String assetId;
final AnalyticsBloc analyticsBloc;
CoinAddressesBloc(
this.sdk,
this.assetId,
this.analyticsBloc,
) : super(const CoinAddressesState()) {
on<SubmitCreateAddressEvent>(_onSubmitCreateAddress);
on<LoadAddressesEvent>(_onLoadAddresses);
on<UpdateHideZeroBalanceEvent>(_onUpdateHideZeroBalance);

StreamSubscription<dynamic>? _pubkeysSub;
CoinAddressesBloc(this.sdk, this.assetId, this.analyticsBloc)
: super(const CoinAddressesState()) {
on<CoinAddressesAddressCreationSubmitted>(_onCreateAddressSubmitted);
on<CoinAddressesStarted>(_onStarted);
on<CoinAddressesSubscriptionRequested>(_onAddressesSubscriptionRequested);
on<CoinAddressesZeroBalanceVisibilityChanged>(_onHideZeroBalanceChanged);
on<CoinAddressesPubkeysUpdated>(_onPubkeysUpdated);
on<CoinAddressesPubkeysSubscriptionFailed>(_onPubkeysSubscriptionFailed);
}

Future<void> _onStarted(
CoinAddressesStarted event,
Emitter<CoinAddressesState> emit,
) async {
add(const CoinAddressesSubscriptionRequested());
}

Future<void> _onSubmitCreateAddress(
SubmitCreateAddressEvent event,
Future<void> _onCreateAddressSubmitted(
CoinAddressesAddressCreationSubmitted event,
Emitter<CoinAddressesState> emit,
) async {
emit(
Expand Down Expand Up @@ -52,7 +64,7 @@ class CoinAddressesBloc extends Bloc<CoinAddressesEvent, CoinAddressesState> {
);
}

add(const LoadAddressesEvent());
add(const CoinAddressesSubscriptionRequested());

emit(
state.copyWith(
Expand Down Expand Up @@ -85,8 +97,8 @@ class CoinAddressesBloc extends Bloc<CoinAddressesEvent, CoinAddressesState> {
}
}

Future<void> _onLoadAddresses(
LoadAddressesEvent event,
Future<void> _onAddressesSubscriptionRequested(
CoinAddressesSubscriptionRequested event,
Emitter<CoinAddressesState> emit,
) async {
emit(state.copyWith(status: () => FormStatus.submitting));
Expand All @@ -104,6 +116,8 @@ class CoinAddressesBloc extends Bloc<CoinAddressesEvent, CoinAddressesState> {
cantCreateNewAddressReasons: () => reasons,
),
);

_startWatchingPubkeys(asset);
} catch (e) {
emit(
state.copyWith(
Expand All @@ -114,10 +128,63 @@ class CoinAddressesBloc extends Bloc<CoinAddressesEvent, CoinAddressesState> {
}
}

void _onUpdateHideZeroBalance(
UpdateHideZeroBalanceEvent event,
void _onHideZeroBalanceChanged(
CoinAddressesZeroBalanceVisibilityChanged event,
Emitter<CoinAddressesState> emit,
) {
emit(state.copyWith(hideZeroBalance: () => event.hideZeroBalance));
}

Future<void> _onPubkeysUpdated(
CoinAddressesPubkeysUpdated event,
Emitter<CoinAddressesState> 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<CoinAddressesState> 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<void> close() async {
await _pubkeysSub?.cancel();
_pubkeysSub = null;
return super.close();
}
}
35 changes: 29 additions & 6 deletions lib/bloc/coin_addresses/bloc/coin_addresses_event.dart
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -7,19 +8,41 @@ abstract class CoinAddressesEvent extends Equatable {
List<Object?> 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<Object?> get props => [hideZeroBalance];
}

/// Emitted when the pubkeys watcher emits an updated set of keys (and balances)
class CoinAddressesPubkeysUpdated extends CoinAddressesEvent {
final List<PubkeyInfo> addresses;
const CoinAddressesPubkeysUpdated(this.addresses);

@override
List<Object?> get props => [addresses];
}

/// Emitted when the pubkeys watcher reports an error
class CoinAddressesPubkeysSubscriptionFailed extends CoinAddressesEvent {
final String error;
const CoinAddressesPubkeysSubscriptionFailed(this.error);

@override
List<Object?> get props => [error];
}
24 changes: 5 additions & 19 deletions lib/shared/widgets/coin_balance.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,15 @@ 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;

@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;
Expand All @@ -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),
],
),
Expand Down
47 changes: 25 additions & 22 deletions lib/shared/widgets/coin_fiat_balance.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<BalanceInfo>(
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);
},
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -243,7 +244,6 @@ class AddressCard extends StatelessWidget {
),
const SizedBox(height: 12),
_Balance(address: address, coin: coin),
const SizedBox(height: 4),
],
)
: SizedBox(
Expand Down Expand Up @@ -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);
Comment on lines +289 to +292
Copy link
Copy Markdown
Collaborator

@CharlVS CharlVS Aug 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a stream-based approach so that we're resilient to edge cases where the user is on the page before the prices are loaded or if they have a momentary dip in their connection.

I generally avoid using stream builders because they clutter up the UI and suggest that the state is modelled correctly incorrectly, but all fiat-display UI code is going to be overhauled in the near future when we add the UX enhancement for multiple fiat currency options.

EDIT: strike out and replace a word which changes the meaning of the entire comment (@smk762 @takenagain)


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),
);
}
Expand Down Expand Up @@ -642,7 +646,7 @@ class HideZeroBalanceCheckbox extends StatelessWidget {
value: hideZeroBalance,
onChanged: (value) {
context.read<CoinAddressesBloc>().add(
UpdateHideZeroBalanceEvent(value),
CoinAddressesZeroBalanceVisibilityChanged(value),
);
},
);
Expand Down Expand Up @@ -683,7 +687,7 @@ class CreateButton extends StatelessWidget {
createAddressStatus != FormStatus.submitting
? () {
context.read<CoinAddressesBloc>().add(
const SubmitCreateAddressEvent(),
const CoinAddressesAddressCreationSubmitted(),
);
}
: null,
Expand Down
Loading
Loading