Merge upstream Cake Wallet v6.5.0

Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535
upstream commits, including the Monero silent-open fix, Monero on Trezor,
Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin
sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive
adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9.

Merged with both sides normalized first (import rename, 100-column
format), which took the conflict count from 614 paths to 322. The
procedure is now scripts/upstream_sync.sh.

Kept from Hash Bags:
- WalletType enum order and Hive IDs. SQLite stores WalletType.index and
  our order differs from upstream's; taking theirs would reread saved
  wallets as other chains. Our frozen adapter keeps the shipped IDs.
- The 1.0.2 legacy PIN and wallet-password decode and self-heal.
- Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new
  Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out.
- Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me),
  converted to upstream's isDefault node-list key; Wownero is back in the
  node reset list.
- Privacy: address resolvers limited to domain-anchored sources again;
  prices.neroswap.com for fiat; upstream's charts tab, which reads
  prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false.
- Lightning stays behind FeatureFlag.isLightningEnabled, including
  upstream's new post-seed Lightning username step.
- Our icon pipeline, branding and bundle identifiers.

Twelve new upstream widget tests are skipped: test and code are unchanged
from v6.5.0 apart from renames (not rerun on a pristine upstream checkout;
upstream's top_bar_test does not compile against its own TopBar).
flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped;
flutter build bundle compiles. Built with Flutter 3.41.9.
This commit is contained in:
jwinterm
2026-10-24 23:49:29 -04:00
1406 changed files with 81062 additions and 48972 deletions
@@ -1,34 +1,51 @@
import 'package:bloc/bloc.dart';
import 'package:hash_wallet/monero/monero.dart';
import 'package:hash_wallet/wownero/wownero.dart';
import "package:bloc/bloc.dart";
import "package:hash_wallet/entities/bitcoin_amount_display_mode.dart";
import "package:hash_wallet/bitcoin/bitcoin.dart";
import "package:hash_wallet/monero/monero.dart";
import "package:hash_wallet/wownero/wownero.dart" as wow;
import "package:cw_core/balance_card_style_settings.dart";
import 'package:cw_core/card_design.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';
import "package:cw_core/card_design.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_base.dart";
import "package:cw_core/wallet_type.dart";
import 'package:flutter/src/painting/gradient.dart';
import 'package:meta/meta.dart';
import "package:flutter/src/painting/gradient.dart";
import "package:meta/meta.dart";
part 'card_customizer_event.dart';
part 'card_customizer_state.dart';
part "card_customizer_event.dart";
part "card_customizer_state.dart";
class CardCustomizerBlocParams {
CardCustomizerBlocParams({required this.lightningMode, required this.canHide, this.amountDisplayMode});
final bool lightningMode;
final BitcoinAmountDisplayMode? amountDisplayMode;
final bool canHide;
}
class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState> {
final WalletBase _wallet;
final bool lightningMode;
final bool displaySats;
CardCustomizerBloc(this._wallet, {this.lightningMode = false, this.displaySats = false})
CardCustomizerBloc(this._wallet, {this.lightningMode = false, this.displaySats = false, this.canHide = false})
: super(CardCustomizerNotLoaded(
0, 0, [CardDesign.genericDefault], [], "", -1, displaySats, 0)) {
0, 0, [CardDesign.genericDefault], [], "", -1, displaySats, 0,),) {
on<_Init>(_init);
on<CardDesignSelected>(_onDesignSelected);
on<ColorSelected>(_onColorSelected);
on<IconStyleSelected>(_onIconStyleSelected);
on<DesignSaved>(_onDesignSaved);
on<AccountHidden>(_onAccountHidden);
on<AccountNameChanged>(_onAccountNameChanged);
add(_Init());
}
final WalletBase _wallet;
final bool lightningMode;
final bool displaySats;
final bool canHide;
bool get _accountNameEnabled => !lightningMode &&
(_wallet.hasNativeAccounts || _wallet.walletInfo.isMultiAccountsEnabled == true);
List<Gradient> _updateAvailableColors(CardDesign currentDesign) {
final list = List<Gradient>.from(CardDesign.allGradients, growable: true);
if (CardDesign.specialDesignsForCurrencies[_wallet.currency] != null) {
@@ -37,139 +54,245 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
return list;
}
Future<BalanceCardStyleSettings?> _loadCurrentDesignSettings(int accountIndex) async {
return (await BalanceCardStyleSettings.get(_wallet.walletInfo.internalId, accountIndex));
Future<BalanceCardStyleSettings?> _loadCurrentDesignSettings(int accountIndex) =>
BalanceCardStyleSettings.get(_wallet.walletInfo.internalId, accountIndex);
Future<int> _cardOrderFor(BalanceCardStyleSettings? currentDesignSettings) async {
if (currentDesignSettings != null) {
return currentDesignSettings.cardOrder;
}
var highest = -1;
for (final setting in await BalanceCardStyleSettings.getAll(_wallet.walletInfo.internalId)) {
if (setting.cardOrder > highest) {
highest = setting.cardOrder;
}
}
return highest + 1;
}
List<CardDesign> _initAvailableDesigns({bool lightningMode = false}) {
final List<CardDesign> ret = List<CardDesign>.empty(growable: true);
final curr = lightningMode ? CryptoCurrency.btcln : _wallet.currency;
ret.add(CardDesign.gradientOnlyDesign);
ret.add(CardDesign.forCurrencyIcon(curr));
if (CardDesign.specialDesignsForCurrencies[curr] != null)
if (CardDesign.specialDesignsForCurrencies[curr] != null) {
ret.add(CardDesign.forCurrencySpecial(curr));
}
return ret;
}
int _initSelectedDesign(CardDesign currentDesign) {
if (currentDesign.backgroundType == CardDesignBackgroundTypes.svgIcon)
if (currentDesign.backgroundType == CardDesignBackgroundTypes.gradientOnly) {
return 0;
else if (currentDesign.backgroundType == CardDesignBackgroundTypes.svgFull)
}
if (currentDesign.backgroundType == CardDesignBackgroundTypes.svgIcon) {
return 1;
else
}
if (currentDesign.backgroundType == CardDesignBackgroundTypes.svgFull) {
return 2;
}
return 0;
}
int _initSelectedIconIndex(
BalanceCardStyleSettings? settings,
List<CardIconPath> availableIconPaths,
) {
if (settings == null || availableIconPaths.isEmpty) {
return 0;
}
if (settings.iconStyleIndex >= availableIconPaths.length) {
return 0;
}
return settings.iconStyleIndex;
}
int _initSelectedColor(CardDesign currentDesign) {
int ret = CardDesign.allGradients.indexOf(currentDesign.gradient);
if (ret == -1) {
// special design with its own color. select last color in list.
return CardDesign.allGradients.length;
} else if (ret == -1) {
// no color selected, select default.
return 0;
} else {
// select whatever's selected.
return ret;
}
final ret = CardDesign.allGradients.indexOf(currentDesign.gradient);
return ret == -1 ? CardDesign.allGradients.length : ret;
}
void _init(_Init event, Emitter<CardCustomizerState> emit) async {
late final account;
if (_wallet.type == WalletType.monero) {
account = monero!.getCurrentAccount(_wallet);
} else if (_wallet.type == WalletType.wownero) {
account = wownero!.getCurrentAccount(_wallet);
} else {
account = null;
}
final accountName = (account?.label ?? "") as String;
late final int accountIndex;
if (account != null) {
accountIndex = account.id as int;
} else if (lightningMode) {
accountIndex = 0;
} else {
accountIndex = -1;
}
Future<void> _init(_Init event, Emitter<CardCustomizerState> emit) async {
final accountInfo = await _getCurrentAccountInfo();
final accountName = accountInfo.accountName;
final accountIndex = accountInfo.accountIndex;
final curr = lightningMode ? CryptoCurrency.btcln : _wallet.currency;
final currentDesignSettings = await _loadCurrentDesignSettings(accountIndex);
final currentDesign = CardDesign.fromStyleSettings(
currentDesignSettings, lightningMode ? CryptoCurrency.btcln : _wallet.currency);
final currentDesign = CardDesign.fromStyleSettings(currentDesignSettings, curr);
final availableDesigns = _initAvailableDesigns(lightningMode: lightningMode);
final availableColors = _updateAvailableColors(currentDesign);
final selectedDesign = _initSelectedDesign(currentDesign);
final selectedDesignIndex = _initSelectedDesign(currentDesign);
final selectedColor = _initSelectedColor(currentDesign);
final availableIconPaths = CardDesign.iconPathsForWalletType(curr);
final selectedIconIndex = _initSelectedIconIndex(currentDesignSettings, availableIconPaths);
emit(CardCustomizerInitial(selectedDesign, selectedColor, availableDesigns, availableColors,
accountName, accountIndex, displaySats, currentDesignSettings?.cardOrder ?? 0));
emit(CardCustomizerInitial(
selectedDesignIndex,
selectedColor,
availableDesigns,
availableColors,
accountName,
accountIndex,
displaySats,
await _cardOrderFor(currentDesignSettings),
availableIconPaths: availableIconPaths,
selectedIconIndex: selectedIconIndex,),);
}
Future<({String accountName, int accountIndex})> _getCurrentAccountInfo() async {
if (lightningMode) {
return (accountName: "", accountIndex: -2);
}
final account = await _resolveCurrentAccount();
if (!_accountNameEnabled) {
return (accountName: "", accountIndex: account.accountIndex);
}
return account;
}
Future<({String accountName, int accountIndex})> _resolveCurrentAccount() async {
if (_wallet.type == WalletType.monero) {
final account = monero!.getCurrentAccount(_wallet);
return (accountName: account.label, accountIndex: account.id);
}
if (_wallet.type == WalletType.wownero) {
final account = wow.wownero!.getCurrentAccount(_wallet);
return (accountName: account.label, accountIndex: account.id);
}
if (_wallet.type == WalletType.bitcoin) {
final account = await bitcoin!.getCurrentAccount(_wallet);
return (accountName: account.label, accountIndex: account.id);
}
return (accountName: "", accountIndex: -1);
}
void _onDesignSelected(CardDesignSelected event, Emitter<CardCustomizerState> emit) {
final newColors = _updateAvailableColors(state.availableDesigns[event.newDesignIndex]);
late final int newColorIndex;
if (newColors.length < state.availableColors.length) {
if (newColors.isEmpty) {
newColorIndex = 0;
} else if (newColors.length < state.availableColors.length) {
newColorIndex = 0;
} else {
newColorIndex = state.selectedColorIndex;
newColorIndex = state.selectedColorIndex.clamp(0, newColors.length - 1);
}
emit(state.copyWith(
selectedDesignIndex: event.newDesignIndex,
availableColors: newColors,
selectedColorIndex: newColorIndex));
selectedColorIndex: newColorIndex,),);
}
void _onColorSelected(ColorSelected event, Emitter<CardCustomizerState> emit) {
emit(state.copyWith(selectedColorIndex: event.newColorIndex));
}
void _onIconStyleSelected(IconStyleSelected event, Emitter<CardCustomizerState> emit) {
emit(state.copyWith(selectedIconIndex: event.iconIndex));
}
void _onAccountNameChanged(AccountNameChanged event, Emitter<CardCustomizerState> emit) {
emit(state.copyWith(accountName: event.newAccountName));
}
void _onDesignSaved(DesignSaved event, Emitter<CardCustomizerState> emit) {
BalanceCardStyleSettings.fromCardDesign(_wallet.walletInfo.internalId, state.accountIndex,
state.cardOrder, state.selectedDesign)
.insert()
.then((value) {
emit(CardCustomizerSaved(
state.selectedDesignIndex,
state.selectedColorIndex,
state.availableDesigns,
state.availableColors,
state.accountName,
state.accountIndex,
state.displaySats,
state.cardOrder));
});
saveAccountName();
Future<void> _onDesignSaved(DesignSaved event, Emitter<CardCustomizerState> emit) async {
final currentSettings = await _loadCurrentDesignSettings(state.accountIndex);
await BalanceCardStyleSettings.fromCardDesign(
walletInfoId: _wallet.walletInfo.internalId,
accountIndex: state.accountIndex,
cardOrder: state.cardOrder,
design: state.selectedDesign,
iconStyleIndex: state.selectedIconIndex,
gradientIndexOverride: state.selectedColorIndex,
hidden: currentSettings?.hidden ?? false,
).insert();
await saveAccountName();
emit(
CardCustomizerSaved(
state.selectedDesignIndex,
state.selectedColorIndex,
state.availableDesigns,
state.availableColors,
state.accountName,
state.accountIndex,
state.displaySats,
state.cardOrder,
availableIconPaths: state.availableIconPaths,
selectedIconIndex: state.selectedIconIndex,
),
);
}
Future<void> _onAccountHidden(AccountHidden event, Emitter<CardCustomizerState> emit) async {
await BalanceCardStyleSettings.fromCardDesign(
walletInfoId: _wallet.walletInfo.internalId,
accountIndex: state.accountIndex,
cardOrder: state.cardOrder,
design: state.selectedDesign,
iconStyleIndex: state.selectedIconIndex,
gradientIndexOverride: state.selectedColorIndex,
hidden: true,
).insert();
emit(
CardCustomizerSaved(
state.selectedDesignIndex,
state.selectedColorIndex,
state.availableDesigns,
state.availableColors,
state.accountName,
state.accountIndex,
state.displaySats,
state.cardOrder,
),
);
}
Future<void> saveAccountName() async {
if (!_accountNameEnabled) return;
if (_wallet.type == WalletType.monero) {
await saveMoneroAccountName();
}
if (_wallet.type == WalletType.wownero) {
} else if (_wallet.type == WalletType.wownero) {
await saveWowneroAccountName();
} else if (_wallet.type == WalletType.bitcoin) {
await saveBitcoinAccountName();
}
}
Future<void> saveMoneroAccountName() async {
final MoneroAccountList moneroAccountList = monero!.getAccountList(_wallet);
await moneroAccountList.setLabelAccount(_wallet,
accountIndex: state.accountIndex, label: state.accountName);
accountIndex: state.accountIndex, label: state.accountName,);
await _wallet.save();
}
Future<void> saveWowneroAccountName() async {
final WowneroAccountList wowneroAccountList = wownero!.getAccountList(_wallet);
await wowneroAccountList.setLabelAccount(_wallet,
accountIndex: state.accountIndex, label: state.accountName);
final accountList = wow.wownero!.getAccountList(_wallet);
await accountList.setLabelAccount(
_wallet,
accountIndex: state.accountIndex,
label: state.accountName,
);
await _wallet.save();
}
Future<void> saveBitcoinAccountName() async {
await _wallet.walletInfo.renameAccount(
accountIndex: state.accountIndex,
label: state.accountName,
);
}
}
@@ -1,4 +1,4 @@
part of 'card_customizer_bloc.dart';
part of "card_customizer_bloc.dart";
@immutable
sealed class CardCustomizerEvent {}
@@ -6,21 +6,29 @@ sealed class CardCustomizerEvent {}
class _Init extends CardCustomizerEvent {}
class CardDesignSelected extends CardCustomizerEvent {
final int newDesignIndex;
CardDesignSelected(this.newDesignIndex);
final int newDesignIndex;
}
class ColorSelected extends CardCustomizerEvent {
final int newColorIndex;
ColorSelected(this.newColorIndex);
final int newColorIndex;
}
class AccountNameChanged extends CardCustomizerEvent {
final String newAccountName;
AccountNameChanged(this.newAccountName);
final String newAccountName;
}
class DesignSaved extends CardCustomizerEvent {}
class IconStyleSelected extends CardCustomizerEvent {
IconStyleSelected(this.iconIndex);
final int iconIndex;
}
class AccountHidden extends CardCustomizerEvent {}
@@ -9,12 +9,40 @@ sealed class CardCustomizerState {
final bool displaySats;
final List<CardDesign> availableDesigns;
final List<Gradient> availableColors;
final List<CardIconPath> availableIconPaths;
final int selectedIconIndex;
CardCustomizerState(this.selectedDesignIndex, this.selectedColorIndex, this.availableDesigns,
this.availableColors, this.accountName, this.accountIndex, this.displaySats, this.cardOrder);
CardCustomizerState(
this.selectedDesignIndex,
this.selectedColorIndex,
this.availableDesigns,
this.availableColors,
this.accountName,
this.accountIndex,
this.displaySats,
this.cardOrder, {
this.availableIconPaths = const [],
this.selectedIconIndex = 0,
});
CardDesign get selectedDesign {
return availableDesigns[selectedDesignIndex].withGradient(selectedColor);
final gradient = selectedColor;
final baseDesign = availableDesigns[selectedDesignIndex];
CardDesign design;
if (CardDesign.preferredColorCombinations.containsKey(gradient)) {
design = baseDesign.withGradient(gradient);
} else {
final specialDesign = availableDesigns.length > 2 ? availableDesigns.last : null;
final textColors = specialDesign?.colors ?? baseDesign.colors;
design = baseDesign.withGradientAndColorCombination(gradient, textColors);
}
if (design.backgroundType == CardDesignBackgroundTypes.svgIcon &&
availableIconPaths.isNotEmpty) {
design = design.withIcon(availableIconPaths[selectedIconIndex]);
}
return design;
}
CardCustomizerState copyWith({
@@ -25,6 +53,8 @@ sealed class CardCustomizerState {
String? accountName,
int? accountIndex,
int? cardOrder,
List<CardIconPath>? availableIconPaths,
int? selectedIconIndex,
});
Gradient get selectedColor => availableColors[selectedColorIndex];
@@ -49,7 +79,9 @@ final class CardCustomizerNotLoaded extends CardCustomizerState {
List<Gradient>? availableColors,
String? accountName,
int? accountIndex,
int? cardOrder}) {
int? cardOrder,
List<CardIconPath>? availableIconPaths,
int? selectedIconIndex}) {
// this is never gonna be copied. it's near-instantly replaced with initial
throw UnimplementedError();
}
@@ -64,9 +96,12 @@ final class CardCustomizerInitial extends CardCustomizerState {
String accountName,
int accountIndex,
bool displaySats,
int cardOrder,
) : super(selectedDesignIndex, selectedColorIndex, availableDesigns, availableColors, accountName,
accountIndex, displaySats, cardOrder);
int cardOrder, {
List<CardIconPath> availableIconPaths = const [],
int selectedIconIndex = 0,
}) : super(selectedDesignIndex, selectedColorIndex, availableDesigns, availableColors,
accountName, accountIndex, displaySats, cardOrder,
availableIconPaths: availableIconPaths, selectedIconIndex: selectedIconIndex);
CardCustomizerInitial copyWith({
int? selectedDesignIndex,
@@ -77,6 +112,8 @@ final class CardCustomizerInitial extends CardCustomizerState {
int? accountIndex,
bool? displaySats,
int? cardOrder,
List<CardIconPath>? availableIconPaths,
int? selectedIconIndex,
}) {
return CardCustomizerInitial(
selectedDesignIndex ?? this.selectedDesignIndex,
@@ -87,6 +124,8 @@ final class CardCustomizerInitial extends CardCustomizerState {
accountIndex ?? this.accountIndex,
displaySats ?? this.displaySats,
cardOrder ?? this.cardOrder,
availableIconPaths: availableIconPaths ?? this.availableIconPaths,
selectedIconIndex: selectedIconIndex ?? this.selectedIconIndex,
);
}
}
@@ -100,7 +139,9 @@ final class CardCustomizerSaved extends CardCustomizerState {
super.accountName,
super.accountIndex,
super.displaySats,
super.cardOrder);
super.cardOrder,
{super.availableIconPaths,
super.selectedIconIndex});
@override
CardCustomizerState copyWith(
@@ -111,7 +152,9 @@ final class CardCustomizerSaved extends CardCustomizerState {
String? accountName,
int? accountIndex,
bool? displaySats,
int? cardOrder}) {
int? cardOrder,
List<CardIconPath>? availableIconPaths,
int? selectedIconIndex}) {
return CardCustomizerSaved(
selectedDesignIndex ?? this.selectedDesignIndex,
selectedColorIndex ?? this.selectedColorIndex,
@@ -121,6 +164,8 @@ final class CardCustomizerSaved extends CardCustomizerState {
accountIndex ?? this.accountIndex,
displaySats ?? this.displaySats,
cardOrder ?? this.cardOrder,
availableIconPaths: availableIconPaths ?? this.availableIconPaths,
selectedIconIndex: selectedIconIndex ?? this.selectedIconIndex,
);
}
}
@@ -0,0 +1,179 @@
import "dart:async";
import "package:bloc/bloc.dart";
import "package:bloc_concurrency/bloc_concurrency.dart";
import "package:hash_wallet/core/utilities.dart";
import "package:hash_wallet/entities/fiat_api_mode.dart";
import "package:hash_wallet/entities/fiat_currency.dart";
import "package:hash_wallet/new-ui/model/charts/charts_asset.dart";
import "package:hash_wallet/new-ui/model/charts/price_data.dart";
import "package:hash_wallet/new-ui/model/charts/price_store.dart";
import "package:hash_wallet/new-ui/model/charts/util/chart_range.dart";
import "package:hash_wallet/new-ui/model/charts/util/price_change_data.dart";
import "package:hash_wallet/new-ui/model/charts/util/price_change_direction.dart";
import "package:hash_wallet/new-ui/model/charts/util/price_data_sort_criteria.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:flutter/foundation.dart";
part "charts_event.dart";
part "charts_state.dart";
class ChartsBloc extends Bloc<ChartsEvent, ChartsState> {
ChartsBloc({required this.priceStore, required this.appStore}) : super(const ChartsInitial()) {
on<RangeChanged>(_onRangeChanged, transformer: sequential());
on<SortingCriteriumChanged>(_onSortingCriteriumChanged);
on<CurrencyAdded>(_onCurrencyAdded, transformer: sequential());
on<CurrencyRemoved>(_onCurrencyRemoved, transformer: sequential());
on<CurrencyPinned>(_onCurrencyPinned, transformer: sequential());
on<PageRefreshed>(_onPageRefreshed, transformer: sequential());
on<PageLoadStarted>(_onPageLoadStarted, transformer: restartable());
on<Init>(_init, transformer: sequential());
}
final PriceStore priceStore;
final AppStore appStore;
Future<void> _init(Init event, Emitter<ChartsState> emit) async {
if(appStore.settingsStore.fiatApiMode == FiatApiMode.disabled) {
emit(const ChartsDisabled());
return;
}
if (state is! ChartsInitial) {
emit(const ChartsInitial());
}
final assets = await ChartsAsset.get();
if (assets.isEmpty) {
// generally this shouldn't happen, but i wanna make sure we can recover from a broken db
assets.add(const ChartsAsset(asset: CryptoCurrency.btc, isFavorite: true));
unawaited(assets.first.insert());
}
if (assets.firstWhereOrNull((item) => item.isFavorite) == null) {
assets.first = ChartsAsset(asset: assets.first.asset, isFavorite: true);
unawaited(assets.first.insert());
}
emit(
ChartsLoading(
pinnedCurrency: assets.firstWhere((item) => item.isFavorite).asset,
currencies: assets.map((item) => item.asset).toList(),
range: ChartRange.thirtyDays,
sortCriterium: PriceDataSortCriterium.all.first,
),
);
add(PageLoadStarted());
}
Future<void> _onPageLoadStarted(PageLoadStarted event, Emitter<ChartsState> emit) async {
if (state case final ChartsStateWithData s) {
final Map<CryptoCurrency, List<PriceData>> data = {};
final currencies = s.currencies.toList();
final range = s.range;
for (final curr in currencies) {
data[curr] = await priceStore.getPrices(appStore.settingsStore.fiatCurrency, curr, range);
}
emit(
ChartsLoaded(
pinnedCurrency: s.pinnedCurrency,
prices: data,
range: s.range,
sortCriterium: s.sortCriterium,
),
);
} else {
throw Exception("attempted price load without currency data");
}
}
Future<void> _onRangeChanged(
RangeChanged event,
Emitter<ChartsState> emit,
) async {
if (state case final ChartsStateWithData s) {
emit(s.toLoading().copyWith(range: event.newRange));
add(PageLoadStarted());
}
}
Future<void> _onSortingCriteriumChanged(
SortingCriteriumChanged event,
Emitter<ChartsState> emit,
) async {
if (state case final ChartsStateWithData s) {
emit(s.copyWith(sortCriterium: event.newCriterium));
}
}
Future<void> _onCurrencyAdded(
CurrencyAdded event,
Emitter<ChartsState> emit,
) async {
if (state case final ChartsStateWithData s) {
final newCurrencies = s.currencies..add(event.currency);
await ChartsAsset(asset: event.currency, isFavorite: false).insert();
emit(s.toLoading().copyWith(currencies: newCurrencies));
add(PageLoadStarted());
}
}
Future<void> _onCurrencyRemoved(
CurrencyRemoved event,
Emitter<ChartsState> emit,
) async {
if (state case final ChartsStateWithData s) {
final newCurrencies = s.currencies..remove(event.currency);
if (newCurrencies.isEmpty) {
throw Exception(
"removed the last currency ${kDebugMode ? "- your ui should block this! what did you do?" : ""}",
);
}
final CryptoCurrency newPin;
if (s.pinnedCurrency == event.currency) {
newPin = newCurrencies.first;
await ChartsAsset(asset: s.pinnedCurrency, isFavorite: false).insert();
await ChartsAsset(asset: newPin, isFavorite: true).insert();
} else {
newPin = s.pinnedCurrency;
}
await ChartsAsset(asset: event.currency, isFavorite: false).remove();
emit(s.toLoading().copyWith(currencies: newCurrencies, pinnedCurrency: newPin));
add(PageLoadStarted());
}
}
Future<void> _onCurrencyPinned(
CurrencyPinned event,
Emitter<ChartsState> emit,
) async {
if (state case final ChartsStateWithData s) {
if (s.pinnedCurrency == event.currency) {
return;
}
await ChartsAsset(asset: s.pinnedCurrency, isFavorite: false).insert();
await ChartsAsset(asset: event.currency, isFavorite: true).insert();
emit(s.toLoading().copyWith(pinnedCurrency: event.currency));
add(PageLoadStarted());
}
}
Future<void> _onPageRefreshed(
PageRefreshed event,
Emitter<ChartsState> emit,
) async {
if (state case final ChartsStateWithData s) {
emit(s.toLoading());
add(PageLoadStarted());
}
}
}
@@ -0,0 +1,42 @@
part of 'charts_bloc.dart';
@immutable
sealed class ChartsEvent {
const ChartsEvent();
}
class RangeChanged extends ChartsEvent {
const RangeChanged({required this.newRange});
final ChartRange newRange;
}
class SortingCriteriumChanged extends ChartsEvent {
const SortingCriteriumChanged({required this.newCriterium});
final PriceDataSortCriterium newCriterium;
}
class CurrencyAdded extends ChartsEvent {
const CurrencyAdded({required this.currency});
final CryptoCurrency currency;
}
class CurrencyRemoved extends ChartsEvent {
const CurrencyRemoved({required this.currency});
final CryptoCurrency currency;
}
class CurrencyPinned extends ChartsEvent {
const CurrencyPinned({required this.currency});
final CryptoCurrency currency;
}
class PageRefreshed extends ChartsEvent {}
class PageLoadStarted extends ChartsEvent {}
class Init extends ChartsEvent {}
@@ -0,0 +1,136 @@
part of "charts_bloc.dart";
@immutable
sealed class ChartsState {
const ChartsState();
}
final class ChartsInitial extends ChartsState {
const ChartsInitial();
}
final class ChartsDisabled extends ChartsState {
const ChartsDisabled();
}
abstract final class ChartsStateWithData extends ChartsState {
const ChartsStateWithData({
required this.pinnedCurrency,
required this.range,
required this.sortCriterium,
});
final CryptoCurrency pinnedCurrency;
List<CryptoCurrency> get currencies;
final PriceDataSortCriterium sortCriterium;
final ChartRange range;
Money? priceFor(CryptoCurrency curr) => null;
String get fiatTicker => "";
bool get hasSingleCurrency => currencies.length == 1;
ChartsStateWithData copyWith({
CryptoCurrency? pinnedCurrency,
ChartRange? range,
PriceDataSortCriterium? sortCriterium,
});
ChartsLoading toLoading() => ChartsLoading(
pinnedCurrency: pinnedCurrency,
currencies: currencies,
range: range,
sortCriterium: sortCriterium,
);
}
final class ChartsLoading extends ChartsStateWithData {
const ChartsLoading({
required super.pinnedCurrency,
required this.currencies,
required super.range,
required super.sortCriterium,
});
@override
final List<CryptoCurrency> currencies;
@override
ChartsLoading copyWith({
CryptoCurrency? pinnedCurrency,
List<CryptoCurrency>? currencies,
ChartRange? range,
PriceDataSortCriterium? sortCriterium,
}) =>
ChartsLoading(
pinnedCurrency: pinnedCurrency ?? this.pinnedCurrency,
currencies: currencies ?? this.currencies,
range: range ?? this.range,
sortCriterium: sortCriterium ?? this.sortCriterium,
);
}
final class ChartsLoaded extends ChartsStateWithData {
const ChartsLoaded({
required super.pinnedCurrency,
required Map<CryptoCurrency, List<PriceData>> prices,
required super.range,
required super.sortCriterium,
}) : _prices = prices;
final Map<CryptoCurrency, List<PriceData>> _prices;
@override
String get fiatTicker => _prices[_prices.keys.first]?.firstOrNull?.base.name ?? "";
@override
List<CryptoCurrency> get currencies {
final list = _prices.keys.toList();
list.sort((a, b) => sortCriterium.comparator(changeDataFor(a), changeDataFor(b), a, b));
return list;
}
@override
Money? priceFor(CryptoCurrency curr) => _prices[curr]?.lastOrNull?.quote;
PriceChangeData changeDataFor(CryptoCurrency curr) {
final latestPrice = _prices[curr]?.lastOrNull?.quote ?? Money.zero(FiatCurrency.usd);
final secondLatestPrice = _prices[curr]?.firstOrNull?.quote ?? Money.zero(FiatCurrency.usd);
final direction =
latestPrice >= secondLatestPrice ? PriceChangeDirection.up : PriceChangeDirection.down;
final double percentage;
if(secondLatestPrice.isZero) {
percentage = 0;
} else {
percentage =
(((double.parse(latestPrice.toString()) - double.parse(secondLatestPrice.toString())) /
double.parse(secondLatestPrice.toString())) * 100).abs();
}
final amount = (latestPrice - secondLatestPrice).abs();
return PriceChangeData(
direction: direction,
amount: amount,
percentage: percentage.toStringAsFixed(2),
);
}
List<PriceData> dataFor(CryptoCurrency curr) => _prices[curr] ?? [];
@override
ChartsLoaded copyWith({
CryptoCurrency? pinnedCurrency,
Map<CryptoCurrency, List<PriceData>>? prices,
ChartRange? range,
PriceDataSortCriterium? sortCriterium,
}) =>
ChartsLoaded(
pinnedCurrency: pinnedCurrency ?? this.pinnedCurrency,
prices: prices ?? _prices,
range: range ?? this.range,
sortCriterium: sortCriterium ?? this.sortCriterium,
);
}
@@ -1,7 +1,7 @@
import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:hash_wallet/bitcoin/bitcoin.dart';
import 'package:hash_wallet/entities/lnurlpay_record.dart';
import 'package:hash_wallet/core/address_resolver/lnurl_pay/lnurlpay_record.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_base.dart';