mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
Cw 1551 quick bitcoin wallet sync (#3446)
* add Wallet Accounts page and integrate UI * account model refactor * add walletInfoAccount table and API * add WalletAccountList API * add multi-account support for Electrum wallets * add Bitcoin lightning card and card style handling * refine account filtering and card selection * update current account balance on wallet changes * update configure.dar * Use named parameters for card design * Add Bitcoin account handling and refactor lookup * refactor dashboard * refactor cards UI and account balance handling * Improve Bitcoin account selection and balances * restrict unspent coin usage to current account * filter transactions by current Bitcoin account * refactor electrum wallet * refactor address generation * add accountIndex to address fetch logic * Update configure.dart * reload transactions on Bitcoin account change * restore multiple BTC accounts and improve fetch * show account balance with in btc * track account index in Bitcoin transactions * discover btc BIP39 accounts during restore * fix account switching and card ordering bugs * fix card customizer * limit account name length in modal * update BTC account list balance display * auto-reformat * formating fix * skip HD map validation * fix 0 balance issue * minor fix * quick bitcoin wallet sync * restore shuffle change output * restore address lookup fix * Update electrum.dart * remove account customizer * Merge branch 'CW-1142-Add-accounts-feature-to-BTC' into CW-1551-Quick-Bitcoin-Wallet-Sync * probe only segwit in account discovery * add account discovery limit and tracking * Improve Electrum sync account handling [skip ci] * remove legacy monero account and address list UI * rework BTC account balance refres * avoid unnecessary BTC account reloads * refactor account header in addresses page * fix blank popup when editing an account * fix: dedupe UTXO balance * minor fix * add Bitcoin current account API * Apply batched suggestions from code review [skip ci] Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * address PR review comments [skip ci] * remove account customizer and fix accounts init * restrict extra BTC accounts to SegWit * add BTC multi-account toggle * Fix BTC sync gating and account balance update * minor fix * Update electrum_wallet.dart * store selected account on WalletInfo * Refine wallet account reset visibility * refactor BTC address prep and move accounts page * refactor account stack and customizer routing * migrate legacy Lightning card styles * unify dashboard account change state * fix account routing and Electrum balances * fix account setup and wallet cards UI * disable Bitcoin Accounts for all hardware * merge conflicts fix * minor fixes * update cards_view [skip ci] * reverted cards view update [skip ci] * fix asset name * moved the transaction filtering into the electrum wallet * localization * disable accounts for watch-only wallets * fix: stop card render from switching the wallet account * fix: restore Lightning transactions in tx history * minor fix * fix: skip duplicate legacy address generation [skip ci] * fix: correct account balance/design mismatch bugs * addressing review comments --------- Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com> Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
co-authored by
Omar Hatem
Robert Malikowski
parent
0503d54290
commit
d7ebf42814
@@ -159,7 +159,7 @@ abstract class BalanceViewModelBase with Store {
|
||||
bool get isEVMCompatible => isEVMCompatibleChain(wallet.type);
|
||||
|
||||
@computed
|
||||
bool get hasAccounts => [WalletType.monero, WalletType.wownero].contains(wallet.type);
|
||||
bool get hasAccounts => wallet.hasAccountsSupport;
|
||||
|
||||
@computed
|
||||
SortBalanceBy get sortBalanceBy => settingsStore.sortBalanceBy;
|
||||
|
||||
@@ -5,6 +5,9 @@ import 'dart:io' show Platform;
|
||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/core/key_service.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
|
||||
import 'package:cw_core/account.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/date_section_item.dart';
|
||||
import "package:cw_core/balance_card_style_settings.dart";
|
||||
import 'package:cake_wallet/core/trade_monitor.dart';
|
||||
@@ -77,6 +80,7 @@ class DashboardViewModel = DashboardViewModelBase with _$DashboardViewModel;
|
||||
abstract class DashboardViewModelBase with Store {
|
||||
DashboardViewModelBase(
|
||||
{required this.balanceViewModel,
|
||||
required this.accountListViewModelFactory,
|
||||
required this.tradeMonitor,
|
||||
required this.appStore,
|
||||
required this.tradesStore,
|
||||
@@ -106,7 +110,7 @@ abstract class DashboardViewModelBase with Store {
|
||||
showDecredInfoCard = wallet.type == WalletType.decred &&
|
||||
(sharedPreferences.getBool(PreferencesKey.showDecredInfoCard) ?? true);
|
||||
showSeedBackupReminder = wallet.walletInfo.showSeedBackupReminder;
|
||||
|
||||
isMultiAccountsEnabled = wallet.walletInfo.isMultiAccountsEnabled == true;
|
||||
name = wallet.name;
|
||||
type = wallet.type;
|
||||
isShowFirstYatIntroduction = false;
|
||||
@@ -115,6 +119,7 @@ abstract class DashboardViewModelBase with Store {
|
||||
unawaited(isBackgroundSyncEnabled());
|
||||
unawaited(isBatteryOptimizationEnabled());
|
||||
unawaited(_loadConstraints());
|
||||
accountListViewModel = accountListViewModelFactory();
|
||||
final _wallet = wallet;
|
||||
|
||||
loadFilterItems();
|
||||
@@ -122,8 +127,7 @@ abstract class DashboardViewModelBase with Store {
|
||||
if (_wallet.type == WalletType.monero) {
|
||||
subname = monero!.getCurrentAccount(_wallet).label;
|
||||
|
||||
_onMoneroAccountChangeReaction = reaction(
|
||||
(_) => monero!.getMoneroWalletDetails(wallet).account,
|
||||
_onAccountChangeReaction = reaction((_) => monero!.getMoneroWalletDetails(wallet).account,
|
||||
(Account account) => _onMoneroAccountChange(_wallet));
|
||||
|
||||
_onMoneroBalanceChangeReaction = reaction(
|
||||
@@ -151,9 +155,9 @@ abstract class DashboardViewModelBase with Store {
|
||||
} else if (_wallet.type == WalletType.wownero) {
|
||||
subname = wow.wownero!.getCurrentAccount(_wallet).label;
|
||||
|
||||
_onMoneroAccountChangeReaction = reaction(
|
||||
_onAccountChangeReaction = reaction(
|
||||
(_) => wow.wownero!.getWowneroWalletDetails(wallet).account,
|
||||
(wow.Account account) => _onMoneroAccountChange(_wallet));
|
||||
(Account account) => _onMoneroAccountChange(_wallet));
|
||||
|
||||
_onMoneroBalanceChangeReaction = reaction(
|
||||
(_) => wow.wownero!.getWowneroWalletDetails(wallet).balance,
|
||||
@@ -179,21 +183,12 @@ abstract class DashboardViewModelBase with Store {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final sortedTransactions = [...wallet.transactionHistory.transactions.values];
|
||||
sortedTransactions.sort((a, b) => a.date.compareTo(b.date));
|
||||
|
||||
transactions = ObservableList.of(
|
||||
sortedTransactions.map(
|
||||
(transaction) => TransactionListItem(
|
||||
transaction: transaction,
|
||||
balanceViewModel: balanceViewModel,
|
||||
appStore: appStore,
|
||||
key: ValueKey('${_wallet.type.name}_transaction_history_item_${transaction.id}_key'),
|
||||
),
|
||||
),
|
||||
);
|
||||
subname = "";
|
||||
_reloadTransactions();
|
||||
}
|
||||
|
||||
_setupBitcoinAccountChangeReaction(_wallet);
|
||||
|
||||
// TODO: nano sub-account generation is disabled:
|
||||
// if (_wallet.type == WalletType.nano || _wallet.type == WalletType.banano) {
|
||||
// subname = nano!.getCurrentAccount(_wallet).label;
|
||||
@@ -201,7 +196,9 @@ abstract class DashboardViewModelBase with Store {
|
||||
|
||||
_walletChangeDisposer?.reaction.dispose();
|
||||
_walletChangeDisposer = reaction((_) => appStore.wallet, (wallet) {
|
||||
accountListViewModel = accountListViewModelFactory();
|
||||
_onWalletChange(wallet);
|
||||
resetLightningMode();
|
||||
_checkMweb();
|
||||
loadCardDesigns();
|
||||
showDecredInfoCard = wallet?.type == WalletType.decred &&
|
||||
@@ -362,8 +359,14 @@ abstract class DashboardViewModelBase with Store {
|
||||
|
||||
transactions.clear();
|
||||
|
||||
final filteredTransactions = wallet.type == WalletType.bitcoin
|
||||
? bitcoin!.getCurrentAccountBitcoinTransactions(wallet)
|
||||
: wallet.transactionHistory.transactions.values.toList();
|
||||
filteredTransactions.sort((a, b) => a.date.compareTo(b.date));
|
||||
|
||||
|
||||
transactions.addAll(
|
||||
wallet.transactionHistory.transactions.values.map(
|
||||
filteredTransactions.map(
|
||||
(transaction) => TransactionListItem(
|
||||
transaction: transaction,
|
||||
balanceViewModel: balanceViewModel,
|
||||
@@ -425,60 +428,67 @@ abstract class DashboardViewModelBase with Store {
|
||||
|
||||
@action
|
||||
Future<void> loadCardDesigns() async {
|
||||
final accountStyleSettings =
|
||||
final walletCardStyleSettings =
|
||||
await BalanceCardStyleSettings.getAll(wallet.walletInfo.internalId);
|
||||
final btcAccounts =
|
||||
wallet.type == WalletType.bitcoin ? await wallet.walletInfo.getAccounts() : null;
|
||||
final lightningCardIndex = btcAccounts?.length;
|
||||
|
||||
late final int numAccounts;
|
||||
int numAccounts = 1;
|
||||
if (wallet.type == WalletType.monero) {
|
||||
numAccounts = monero!.getAccountList(wallet).accounts.length;
|
||||
} else if (wallet.type == WalletType.wownero) {
|
||||
numAccounts = wow.wownero!.getAccountList(wallet).accounts.length;
|
||||
} else if (wallet.type == WalletType.bitcoin) {
|
||||
// bitcoin and lightning
|
||||
numAccounts = 2;
|
||||
} else {
|
||||
numAccounts = 1;
|
||||
numAccounts = btcAccounts!.length + 1; // adding 1 for the lightning account
|
||||
}
|
||||
|
||||
final usesAccountIndices = wallet.type == WalletType.bitcoin || balanceViewModel.hasAccounts;
|
||||
|
||||
cardDesigns.clear();
|
||||
Map<int, int> newOrder = {};
|
||||
final newOrder = <int, int>{};
|
||||
|
||||
final orderableCount = (wallet.type == WalletType.bitcoin) ? numAccounts - 1 : numAccounts;
|
||||
|
||||
for (int i = 0; i < numAccounts; i++) {
|
||||
late final int index;
|
||||
if (balanceViewModel.hasAccounts) {
|
||||
final isLightning = wallet.type == WalletType.bitcoin && i == lightningCardIndex;
|
||||
if (isLightning) {
|
||||
index = -2;
|
||||
} else if (usesAccountIndices) {
|
||||
index = i;
|
||||
} else if (wallet.type == WalletType.bitcoin && i == 1) {
|
||||
index = 0;
|
||||
} else {
|
||||
index = -1;
|
||||
}
|
||||
|
||||
final setting = accountStyleSettings.where((e) => e.accountIndex == index).firstOrNull;
|
||||
final setting = walletCardStyleSettings.where((e) => e.accountIndex == index).firstOrNull ??
|
||||
(index == 0 && wallet.type != WalletType.bitcoin
|
||||
? walletCardStyleSettings.where((e) => e.accountIndex == -1).firstOrNull
|
||||
: null);
|
||||
|
||||
late final CryptoCurrency curr;
|
||||
if (wallet.type == WalletType.bitcoin && i == 1) {
|
||||
curr = CryptoCurrency.btcln;
|
||||
} else {
|
||||
curr = wallet.currency;
|
||||
}
|
||||
final curr = isLightning ? CryptoCurrency.btcln : wallet.currency;
|
||||
|
||||
cardDesigns.add(CardDesign.fromStyleSettings(setting, curr));
|
||||
if (setting?.cardOrder != null) {
|
||||
newOrder[setting!.cardOrder] = i;
|
||||
|
||||
if (isLightning) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (setting?.cardOrder != null &&
|
||||
setting!.cardOrder >= 0 &&
|
||||
setting.cardOrder < orderableCount) {
|
||||
newOrder[setting.cardOrder] = i;
|
||||
}
|
||||
}
|
||||
|
||||
// making sure ALL accounts have numbers, even the ones that existed before this feature was a thing
|
||||
for (int i = 0; i < numAccounts; i++) {
|
||||
if (!newOrder.containsKey(i) && !(wallet.type != WalletType.bitcoin && i == 1)) {
|
||||
for (int i = 0; i < orderableCount; i++) {
|
||||
if (!newOrder.containsValue(i)) {
|
||||
int free = 0;
|
||||
while (newOrder.containsValue(free)) {
|
||||
while (newOrder.containsKey(free)) {
|
||||
free++;
|
||||
}
|
||||
if (wallet.type == WalletType.bitcoin) {
|
||||
newOrder[free] = 0;
|
||||
} else {
|
||||
newOrder[free] = i;
|
||||
}
|
||||
newOrder[free] = i;
|
||||
}
|
||||
}
|
||||
cardOrder = newOrder.asObservable();
|
||||
@@ -496,19 +506,22 @@ abstract class DashboardViewModelBase with Store {
|
||||
: wallet.type == WalletType.wownero
|
||||
? wow.wownero!.getCurrentAccount(wallet).id
|
||||
: null;
|
||||
final List<TransactionInfo> relevantTxs = [];
|
||||
|
||||
for (final tx in appStore.wallet!.transactionHistory.transactions.values) {
|
||||
bool isRelevant = true;
|
||||
if (wallet.type == WalletType.monero) {
|
||||
isRelevant = monero!.getTransactionInfoAccountId(tx) == currentAccountId;
|
||||
} else if (wallet.type == WalletType.wownero) {
|
||||
isRelevant = wow.wownero!.getTransactionInfoAccountId(tx) == currentAccountId;
|
||||
}
|
||||
final allTxs = appStore.wallet!.transactionHistory.transactions.values;
|
||||
final List<TransactionInfo> relevantTxs;
|
||||
|
||||
if (isRelevant) {
|
||||
relevantTxs.add(tx);
|
||||
}
|
||||
if (wallet.type == WalletType.monero) {
|
||||
relevantTxs = allTxs
|
||||
.where((tx) => monero!.getTransactionInfoAccountId(tx) == currentAccountId)
|
||||
.toList();
|
||||
} else if (wallet.type == WalletType.wownero) {
|
||||
relevantTxs = allTxs
|
||||
.where((tx) => wow.wownero!.getTransactionInfoAccountId(tx) == currentAccountId)
|
||||
.toList();
|
||||
} else if (wallet.type == WalletType.bitcoin) {
|
||||
relevantTxs = bitcoin!.getCurrentAccountBitcoinTransactions(wallet);
|
||||
} else {
|
||||
relevantTxs = allTxs.toList();
|
||||
}
|
||||
// printV("Transaction disposer callback (relevantTxs: ${relevantTxs.length} current: ${transactions.length})");
|
||||
|
||||
@@ -600,6 +613,12 @@ abstract class DashboardViewModelBase with Store {
|
||||
@observable
|
||||
ObservableMap<int, int> cardOrder;
|
||||
|
||||
@observable
|
||||
bool lightningMode = false;
|
||||
|
||||
@observable
|
||||
WalletAccountListViewModel? accountListViewModel;
|
||||
|
||||
@computed
|
||||
bool get isDarkTheme => appStore.themeStore.currentTheme.isDark;
|
||||
|
||||
@@ -896,6 +915,42 @@ abstract class DashboardViewModelBase with Store {
|
||||
@observable
|
||||
late bool showSeedBackupReminder;
|
||||
|
||||
@computed
|
||||
bool get canToggleMultiAccounts => wallet.canToggleMultiAccounts;
|
||||
|
||||
@observable
|
||||
late bool isMultiAccountsEnabled;
|
||||
|
||||
@computed
|
||||
List<AccountListItem> get visibleAccounts {
|
||||
final all = accountListViewModel?.accounts;
|
||||
if (all == null || all.isEmpty) return const <AccountListItem>[];
|
||||
if (isMultiAccountsEnabled) return all.toList(growable: false);
|
||||
final primary = all.where((a) => a.id == 0).firstOrNull ?? all.first;
|
||||
return <AccountListItem>[primary];
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> setMultiAccountsEnabled(bool value) async {
|
||||
if (!canToggleMultiAccounts) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
await _selectPrimaryAccount();
|
||||
}
|
||||
|
||||
wallet.walletInfo.isMultiAccountsEnabled = value;
|
||||
await wallet.walletInfo.save();
|
||||
isMultiAccountsEnabled = value;
|
||||
await accountListViewModel?.reload();
|
||||
await loadCardDesigns();
|
||||
|
||||
if (value) {
|
||||
unawaited(wallet.startSync());
|
||||
}
|
||||
}
|
||||
|
||||
@computed
|
||||
bool get hasBalance => wallet.balance.values.any(
|
||||
(balance) =>
|
||||
@@ -1161,6 +1216,8 @@ abstract class DashboardViewModelBase with Store {
|
||||
bitcoin!.updatePayjoinState(wallet, true);
|
||||
}
|
||||
|
||||
final WalletAccountListViewModel? Function() accountListViewModelFactory;
|
||||
|
||||
BalanceViewModel balanceViewModel;
|
||||
|
||||
TradeMonitor tradeMonitor;
|
||||
@@ -1215,7 +1272,7 @@ abstract class DashboardViewModelBase with Store {
|
||||
@computed
|
||||
bool get isEnabledBulletinAction => !settingsStore.disableBulletin;
|
||||
|
||||
ReactionDisposer? _onMoneroAccountChangeReaction;
|
||||
ReactionDisposer? _onAccountChangeReaction;
|
||||
|
||||
ReactionDisposer? _onMoneroBalanceChangeReaction;
|
||||
|
||||
@@ -1289,6 +1346,31 @@ abstract class DashboardViewModelBase with Store {
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void toggleLightningMode() {
|
||||
lightningMode = !lightningMode;
|
||||
|
||||
// Lightning belongs to the primary account only
|
||||
if (lightningMode) {
|
||||
unawaited(_selectPrimaryAccount());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectPrimaryAccount() async {
|
||||
if (wallet.type != WalletType.bitcoin) return;
|
||||
|
||||
final vm = accountListViewModel;
|
||||
if (vm == null || vm.selectedAccount?.id == 0) return;
|
||||
|
||||
final primary = vm.accounts.where((a) => a.id == 0).firstOrNull;
|
||||
if (primary != null) {
|
||||
await vm.select(primary);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void resetLightningMode() => lightningMode = false;
|
||||
|
||||
@action
|
||||
void _onWalletChange(
|
||||
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>? wallet) {
|
||||
@@ -1299,16 +1381,19 @@ abstract class DashboardViewModelBase with Store {
|
||||
this.wallet = wallet;
|
||||
type = wallet.type;
|
||||
name = wallet.name;
|
||||
isMultiAccountsEnabled = wallet.walletInfo.isMultiAccountsEnabled == true;
|
||||
|
||||
_onAccountChangeReaction?.reaction.dispose();
|
||||
_onAccountChangeReaction = null;
|
||||
loadFilterItems();
|
||||
|
||||
if (wallet.type == WalletType.monero) {
|
||||
subname = monero!.getCurrentAccount(wallet).label;
|
||||
|
||||
_onMoneroAccountChangeReaction?.reaction.dispose();
|
||||
_onAccountChangeReaction?.reaction.dispose();
|
||||
_onMoneroBalanceChangeReaction?.reaction.dispose();
|
||||
|
||||
_onMoneroAccountChangeReaction = reaction(
|
||||
(_) => monero!.getMoneroWalletDetails(wallet).account,
|
||||
_onAccountChangeReaction = reaction((_) => monero!.getMoneroWalletDetails(wallet).account,
|
||||
(Account account) => _onMoneroAccountChange(wallet));
|
||||
|
||||
_onMoneroBalanceChangeReaction = reaction(
|
||||
@@ -1319,12 +1404,12 @@ abstract class DashboardViewModelBase with Store {
|
||||
} else if (wallet.type == WalletType.wownero) {
|
||||
subname = wow.wownero!.getCurrentAccount(wallet).label;
|
||||
|
||||
_onMoneroAccountChangeReaction?.reaction.dispose();
|
||||
_onAccountChangeReaction?.reaction.dispose();
|
||||
_onMoneroBalanceChangeReaction?.reaction.dispose();
|
||||
|
||||
_onMoneroAccountChangeReaction = reaction(
|
||||
_onAccountChangeReaction = reaction(
|
||||
(_) => wow.wownero!.getWowneroWalletDetails(wallet).account,
|
||||
(wow.Account account) => _onMoneroAccountChange(wallet));
|
||||
(Account account) => _onMoneroAccountChange(wallet));
|
||||
|
||||
_onMoneroBalanceChangeReaction = reaction(
|
||||
(_) => wow.wownero!.getWowneroWalletDetails(wallet).balance,
|
||||
@@ -1335,7 +1420,7 @@ abstract class DashboardViewModelBase with Store {
|
||||
// FIX-ME: Check for side effects
|
||||
// subname = null;
|
||||
subname = '';
|
||||
|
||||
_setupBitcoinAccountChangeReaction(wallet);
|
||||
_reloadTransactions();
|
||||
}
|
||||
|
||||
@@ -1378,6 +1463,21 @@ abstract class DashboardViewModelBase with Store {
|
||||
}, _transactionDisposerCallback, delay: 300);
|
||||
}
|
||||
|
||||
void _setupBitcoinAccountChangeReaction(WalletBase wallet) {
|
||||
if (wallet.type != WalletType.bitcoin) {
|
||||
return;
|
||||
}
|
||||
|
||||
_onAccountChangeReaction?.reaction.dispose();
|
||||
_onAccountChangeReaction = reaction(
|
||||
(_) => accountListViewModel?.selectedAccount?.id,
|
||||
(_) {
|
||||
_reloadTransactions();
|
||||
},
|
||||
fireImmediately: true,
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
void _onMoneroAccountChange(WalletBase wallet) {
|
||||
if (wallet.type == WalletType.monero) {
|
||||
|
||||
@@ -55,10 +55,6 @@ class TransactionListItem extends ActionListItem with Keyable {
|
||||
}
|
||||
|
||||
String get formattedTitle {
|
||||
if (balanceViewModel.wallet.type == WalletType.bitcoin &&
|
||||
transaction.additionalInfo['hasMissingInputTx'] == true) {
|
||||
return 'Transaction has missing data';
|
||||
}
|
||||
|
||||
if (transaction.additionalInfo['isIronwoodMigration'] == true) {
|
||||
return 'Migration';
|
||||
|
||||
Reference in New Issue
Block a user