Cw 1551 quick bitcoin wallet sync (#3446)
What changed, and why it matters
This is a large feature commit that adds multi-account support for Bitcoin wallets in Cake Wallet, along with a 'quick sync' optimization. It changes how addresses, transactions, balances, and unspent coins are tracked per account. The changes are mostly architectural, but they touch sensitive wallet logic such as key derivation, transaction selection, balance calculation, and address discovery. There is no explicit vendor statement that this is a security fix, and no CVE or independent researcher attribution is present in the commit materials.
Treat this as a high-risk feature change rather than a routine fix. Reviewers should verify that: (1) account-scoped HD derivation cannot be tricked into deriving keys for the wrong account, (2) transaction signing only uses UTXOs from the current account, (3) the quick-sync background queue does not drop or misattribute transactions across accounts, (4) balance updates remain consistent when switching accounts rapidly, and (5) the new database migrations preserve existing wallet state. End users should update only after the release has been publicly tested and any follow-up fixes are available.
Security signals we found
Multi-account key derivation path now uses accountIndex from address record rather than parsing derivation path, reducing risk of deriving wrong account keys
UTXO selection and transaction building restricted to current account's unspent coins (unspentCoinsForCurrentAccount)
Address generation throws UnsupportedAddressTypeForAccountException for unsupported account/type combinations, preventing accidental key derivation for invalid paths
Balance fetch now preserves last known balance on network failure instead of silently zeroing balances
UTXO balance deduplication added to prevent double-counting the same coin against an address balance
Sync gating added to prevent indefinite re-entrant syncs with a 2-minute stuck timeout
Transaction account ownership resolution retries capped to avoid infinite re-fetch loops for unresolvable transactions
Legacy derivation explicitly disallowed for accountIndex > 0
Lightning and Silent Payments receive options disabled for extra accounts
Database schema version bumped and WalletInfo account tables added
Evidence from the diff
The commit refactors Electrum-based wallets (Bitcoin, Litecoin, Bitcoin Cash, Dogecoin) to support multiple BIP39 accounts for Bitcoin, while other chains keep a single account. Key changes include: adding accountIndex to address records and transactions; replacing per-type HD maps with per-account/per-type maps; restricting extra Bitcoin accounts to SegWit p2wpkh only; adding account discovery during restore; deferring full transaction detail resolution to a background queue; filtering UTXOs and transactions by current account; and adding balance equality/value semantics. Several bug-fix style changes are embedded, such as deduplicating UTXO balance application via an applied-coin-keys set, skipping duplicate legacy address generation, handling null/failed balance fetches, and gating sync with a stuck-sync timeout. The commit also removes the legacy Monero account/address list UI and migrates card styling.
Changed components
cw_bitcoin/lib/electrum_wallet.dartcw_bitcoin/lib/electrum_wallet_addresses.dartcw_bitcoin/lib/bitcoin_wallet.dartcw_bitcoin/lib/bitcoin_address_record.dartcw_bitcoin/lib/electrum_transaction_info.dartcw_bitcoin/lib/pending_bitcoin_transaction.dartcw_bitcoin/lib/litecoin_wallet.dartcw_bitcoin/lib/litecoin_wallet_addresses.dartcw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dartcw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dartcw_dogecoin/lib/src/dogecoin_wallet.dartcw_dogecoin/lib/src/dogecoin_wallet_addresses.dartcw_core/lib/account.dartcw_core/lib/db/sqlite.dartcw_core/lib/wallet_info.dartlib/view_model/dashboard/balance_view_model.dartlib/view_model/wallet_account_list/bitcoin_account_list/bitcoin_account_list_view_model.dartInspect captured patch +3966 / −2161
### cw_bitcoin/lib/bitcoin_address_record.dart
@@ -9,6 +9,7 @@ abstract class BaseBitcoinAddressRecord {
BaseBitcoinAddressRecord(
this.address, {
required this.index,
+ this.accountIndex = 0,
this.isHidden = false,
this.isLegacyDerivation = false,
int txCount = 0,
@@ -29,6 +30,7 @@ abstract class BaseBitcoinAddressRecord {
bool isHidden;
bool isLegacyDerivation;
final int index;
+ final int accountIndex;
int _txCount;
int _balance;
String _name;
@@ -63,6 +65,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
BitcoinAddressRecord(
super.address, {
required super.index,
+ super.accountIndex = 0,
super.isHidden = false,
super.isLegacyDerivation = false,
super.txCount = 0,
@@ -105,6 +108,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
return BitcoinAddressRecord(
decoded['address'] as String,
index: decoded['index'] as int,
+ accountIndex: decoded['accountIndex'] as int? ?? 0,
isHidden: decoded['isHidden'] as bool? ?? false,
isLegacyDerivation: parsedIsLegacy,
isUsed: decoded['isUsed'] as bool? ?? false,
@@ -162,7 +166,8 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
final coinType = _coinTypeForNetwork();
final purpose = _purposeForType(type);
- final accountPath = isLegacyDerivation ? electrum_path : "m/$purpose'/$coinType'/0'";
+ final accountPath =
+ isLegacyDerivation ? electrum_path : "m/$purpose'/$coinType'/$accountIndex'";
final chain = isHidden ? 1 : 0;
return "$accountPath/$chain/$index";
@@ -172,6 +177,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
String toJSON() => json.encode({
'address': address,
'index': index,
+ 'accountIndex': accountIndex,
'isHidden': isHidden,
'isLegacyDerivation': isLegacyDerivation,
'isUsed': isUsed,
@@ -187,6 +193,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
BitcoinSilentPaymentAddressRecord(
super.address, {
required super.index,
+ super.accountIndex = 0,
super.isHidden = false,
super.txCount = 0,
super.balance = 0,
@@ -205,6 +212,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
return BitcoinSilentPaymentAddressRecord(
decoded['address'] as String,
index: decoded['index'] as int,
+ accountIndex: decoded['accountIndex'] as int? ?? 0,
isHidden: decoded['isHidden'] as bool? ?? false,
isUsed: decoded['isUsed'] as bool? ?? false,
txCount: decoded['txCount'] as int? ?? 0,
@@ -233,6 +241,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
String toJSON() => json.encode({
'address': address,
'index': index,
+ 'accountIndex': accountIndex,
'isHidden': isHidden,
'isUsed': isUsed,
'txCount': txCount,
### cw_bitcoin/lib/bitcoin_wallet.dart
@@ -14,6 +14,7 @@ import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_derivations.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
+import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_bitcoin/locktime.dart';
import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart';
@@ -128,8 +129,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
initialChangeAddressIndex: initialChangeAddressIndex,
initialSilentAddresses: initialSilentAddresses,
initialSilentAddressIndex: initialSilentAddressIndex,
- mainHdByType: mainHdByType,
- sideHdByType: sideHdByType,
+ mainHdByTypeAndAccount: mainHdByTypeAndAccount,
+ sideHdByTypeAndAccount: sideHdByTypeAndAccount,
+ accountIndexes: [currentAccountIndex],
+ currentAccountIndex: currentAccountIndex,
legacyMainHd: mainHd,
legacySideHd: sideHd,
network: networkParam ?? network,
@@ -612,10 +615,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
return tx;
}
- List<UtxoWithPrivateKey> getUtxoWithPrivateKeys({bool confirmedOnly = false}) => unspentCoins
- .where((e) => e.isSending && !e.isFrozen && (!confirmedOnly || (e.confirmations ?? 0) > 0))
- .map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this))
- .toList();
+ List<UtxoWithPrivateKey> getUtxoWithPrivateKeys({bool confirmedOnly = false}) =>
+ unspentCoinsForCurrentAccount
+ .where(
+ (e) => e.isSending && !e.isFrozen && (!confirmedOnly || (e.confirmations ?? 0) > 0))
+ .map((unspent) => UtxoWithPrivateKey.fromUnspent(unspent, this))
+ .toList();
Future<void> commitPsbt(String finalizedPsbt) {
final psbt = PsbtV2()..deserializeV0(base64.decode(finalizedPsbt));
@@ -625,6 +630,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
return PendingBitcoinTransaction(
btcTx,
type,
+ accountIndex: currentAccountIndex,
electrumClient: electrumClient,
amount: Money.zero(currency),
fee: Money.zero(currency),
@@ -683,6 +689,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
return PendingBitcoinTransaction(
btcTx,
type,
+ accountIndex: currentAccountIndex,
electrumClient: electrumClient,
amount: Money.zero(currency),
fee: Money.zero(currency),
@@ -716,12 +723,24 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
@override
bool receiveOptionAvailable(ReceivePageOption option) {
- if(option == BitcoinReceivePageOption.lightning) {
- return hasLightningSupport;
+ final isExtraAccount = currentAccountIndex > 0;
+
+ if (option == BitcoinReceivePageOption.lightning) {
+ // Lightning is tied to the primary account only
+ return !isExtraAccount && hasLightningSupport;
}
- if(option == BitcoinReceivePageOption.silent_payments) {
- return hasSilentPaymentsScanning;
+ if (option == BitcoinReceivePageOption.silent_payments) {
+ // Silent payments are tied to the primary account only
+ return !isExtraAccount && hasSilentPaymentsScanning;
+ }
+
+ // Restrict extra Bitcoin accounts (accountIndex > 0) to the allowed address types
+ if (isExtraAccount && option is BitcoinReceivePageOption) {
+ final addressType = option.toType();
+ if (!EXTRA_ACCOUNT_ADDRESS_TYPES.contains(addressType)) {
+ return false;
+ }
}
return true;
### cw_bitcoin/lib/bitcoin_wallet_addresses.dart
@@ -23,8 +23,10 @@ class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAd
abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
BitcoinWalletAddressesBase(
WalletInfo walletInfo, {
- required super.mainHdByType,
- required super.sideHdByType,
+ required super.mainHdByTypeAndAccount,
+ required super.sideHdByTypeAndAccount,
+ required super.accountIndexes,
+ required super.currentAccountIndex,
required super.legacySideHd,
required super.legacyMainHd,
required super.network,
@@ -145,7 +147,11 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
try {
return super
.allAddresses
- .firstWhere((e) => e.type == SegwitAddresType.p2wpkh && !e.isHidden && !e.isUsed)
+ .firstWhere((e) =>
+ e.accountIndex == currentAccountIndex &&
+ e.type == SegwitAddresType.p2wpkh &&
+ !e.isHidden &&
+ !e.isUsed)
.address;
} catch (_) {
return super.addressForBuy;
### cw_bitcoin/lib/electrum_balance.dart
@@ -2,9 +2,17 @@ import 'dart:convert';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/balance.dart';
+import "package:cw_core/crypto_currency.dart";
import 'package:cw_core/currency.dart';
class ElectrumBalance extends Balance {
+
+ factory ElectrumBalance.zero(CryptoCurrency currency) => ElectrumBalance(
+ confirmed: Money.zero(currency),
+ unconfirmed: Money.zero(currency),
+ frozen: Money.zero(currency),
+ );
+
ElectrumBalance({
required this.confirmed,
required this.unconfirmed,
@@ -49,4 +57,26 @@ class ElectrumBalance extends Balance {
'secondConfirmed': secondAvailable?.amount.toInt() ?? 0,
'secondUnconfirmed': secondUnavailable?.amount.toInt() ?? 0,
});
+
+ // Value equality so instances can be compared/deduped (e.g. by MobX reactions via MapEquality)
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ (other is ElectrumBalance &&
+ other.confirmed == confirmed &&
+ other.unconfirmed == unconfirmed &&
+ other.frozen == frozen &&
+ other.secondConfirmed == secondConfirmed &&
+ other.secondUnconfirmed == secondUnconfirmed);
+
+ @override
+ int get hashCode => Object.hash(
+ confirmed,
+ unconfirmed,
+ frozen,
+ secondConfirmed,
+ secondUnconfirmed,
+ );
}
+
+
### cw_bitcoin/lib/electrum_transaction_info.dart
@@ -24,6 +24,7 @@ class ElectrumTransactionBundle {
}
class ElectrumTransactionInfo extends TransactionInfo {
+ int? accountIndex;
List<BitcoinSilentPaymentsUnspent>? unspents;
bool isReceivedSilentPayment;
bool isHogEx;
@@ -41,6 +42,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
bool isReplaced = false,
required DateTime date,
required int confirmations,
+ this.accountIndex,
String? to,
this.unspents,
this.isReceivedSilentPayment = false,
@@ -231,6 +233,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
isPending: data['isPending'] as bool,
isReplaced: data['isReplaced'] as bool? ?? false,
confirmations: data['confirmations'] as int,
+ accountIndex: data['accountIndex'] as int?,
inputAddresses:
inputAddresses.isEmpty ? [] : inputAddresses.map((e) => e.toString()).toList(),
outputAddresses:
@@ -260,6 +263,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
inputAddresses: inputAddresses,
outputAddresses: outputAddresses,
confirmations: info.confirmations,
+ accountIndex: accountIndex,
additionalInfo: additionalInfo);
}
@@ -273,6 +277,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
m['isPending'] = isPending;
m['isReplaced'] = isReplaced;
m['confirmations'] = confirmations;
+ m['accountIndex'] = accountIndex;
m['fee'] = fee?.amount.toInt();
m['to'] = to;
m['unspents'] = unspents?.map((e) => e.toJson()).toList() ?? [];
@@ -284,6 +289,6 @@ class ElectrumTransactionInfo extends TransactionInfo {
}
String toString() {
- return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, isReplaced: $isReplaced, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses, additionalInfo: $additionalInfo)';
+ return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, isReplaced: $isReplaced, confirmations: $confirmations, accountIndex: $accountIndex, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses, additionalInfo: $additionalInfo)';
}
}
### cw_bitcoin/lib/electrum_wallet.dart
@@ -4,6 +4,7 @@ import 'dart:isolate';
import 'dart:math' show Random;
import 'package:bitcoin_base/bitcoin_base.dart';
+import 'package:cw_bitcoin/lightning/lightning_addres_type.dart';
import 'package:cw_bitcoin/lightning/lightning_wallet.dart';
import 'package:cw_bitcoin/locktime.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
@@ -92,66 +93,24 @@ abstract class ElectrumWalletBase
_scripthashesUpdateSubject = {},
this.alwaysScan = alwaysScan,
silentPaymentsScanningActive = alwaysScan ?? false,
- balance = ObservableMap<CryptoCurrency, ElectrumBalance>.of(currency != null
- ? {
- currency: initialBalance ??
- ElectrumBalance(
- confirmed: Money.zero(currency),
- unconfirmed: Money.zero(currency),
- frozen: Money.zero(currency),
- )
- }
- : {}),
+ balance = ObservableMap<CryptoCurrency, ElectrumBalance>.of(
+ currency != null ? {currency: initialBalance ?? ElectrumBalance.zero(currency)} : {}),
this.unspentCoinsInfo = unspentCoinsInfo,
this.isTestnet = !network.isMainnet,
this._mnemonic = mnemonic,
+ this.electrumClient = electrumClient ?? electrum.ElectrumClient(),
_useLightning = useLightning,
super(walletInfo, derivationInfo) {
- this.electrumClient = electrumClient ?? electrum.ElectrumClient();
- this.walletInfo = walletInfo;
- this.derivationInfo = derivationInfo;
transactionHistory = ElectrumTransactionHistory(
walletInfo: walletInfo,
password: password,
encryptionFileUtils: encryptionFileUtils,
);
reaction((_) => syncStatus, _syncStatusReaction);
-
sharedPrefs.complete(SharedPreferences.getInstance());
-
- final supportedTypes = supportedAddressTypes(walletInfo.type);
- mainHdByType = <BitcoinAddressType, Bip32Slip10Secp256k1>{};
- sideHdByType = <BitcoinAddressType, Bip32Slip10Secp256k1>{};
-
- final isElectrumDerivation = derivationInfo.derivationType == DerivationType.electrum;
-
- final canDeriveFromSeed = _masterHD != null && currency != null;
-
- if (isElectrumDerivation) {
- // Electrum derivation does not follow BIP44/49/84 etc. standards
- for (final type in supportedTypes) {
- mainHdByType[type] = mainHd; // accountHD.child(0)
- sideHdByType[type] = sideHd; // accountHD.child(1)
- }
- } else if (canDeriveFromSeed) {
- final coinType = _coinTypeFor(currency);
- final accountIndex = _parseAccountIndex(derivationInfo.derivationPath);
-
- for (final type in supportedTypes) {
- final purpose = _purposeForType(type);
- final accountPath = "m/$purpose'/$coinType'/$accountIndex'";
-
- mainHdByType[type] = _masterHD!.derivePath("$accountPath/0") as Bip32Slip10Secp256k1;
- sideHdByType[type] = _masterHD!.derivePath("$accountPath/1") as Bip32Slip10Secp256k1;
- }
- } else {
- // View-only wallet (xpub only)
- for (final type in supportedTypes) {
- mainHdByType[type] = mainHd;
- sideHdByType[type] = sideHd;
- }
- }
+ // Probably not needed as init will be called right after creating the wallet.
+ _prepareHdForAccount(currentAccountIndex, currency);
}
int _purposeForType(BitcoinAddressType type) {
@@ -200,8 +159,7 @@ abstract class ElectrumWalletBase
final coinType = _coinTypeFor(currency);
final purpose = _purposeForType(record.type);
- final accountIndex = _parseAccountIndex(derivationInfo.derivationPath);
- return "m/$purpose'/$coinType'/$accountIndex'";
+ return "m/$purpose'/$coinType'/${record.accountIndex}'";
}
List<BitcoinAddressType> supportedAddressTypes(WalletType type) {
@@ -317,16 +275,16 @@ abstract class ElectrumWalletBase
static const int addressHistoryChunkSize = 150;
static const int transactionChunkSize = 150;
- static const int inputTransactionChunkSize = 150;
static const int discoveryHistoryChunkSize = 20;
-
static const int transactionBatchTimeoutMs = 15000;
-
static const int batchTestTimeoutMs = 4000;
static const int batchTestHashesCount = 2;
-
static const bool useBatchForHistory = true;
+ static const int maxProbAccounts = 3;
+
+ final Map<String, Set<String>> _appliedCoinKeysByAddress = {};
+
@observable
bool? alwaysScan;
@@ -338,12 +296,15 @@ abstract class ElectrumWalletBase
@observable
bool _useLightning;
+ @observable
+ ObservableMap<int, ElectrumBalance> accountBalances = ObservableMap<int, ElectrumBalance>();
+
final Bip32Slip10Secp256k1? _masterHD;
final Bip32Slip10Secp256k1 accountHD;
final String? _mnemonic;
- late final Map<BitcoinAddressType, Bip32Slip10Secp256k1> mainHdByType;
- late final Map<BitcoinAddressType, Bip32Slip10Secp256k1> sideHdByType;
+ final mainHdByTypeAndAccount = <int, Map<BitcoinAddressType, Bip32Slip10Secp256k1>>{};
+ final sideHdByTypeAndAccount = <int, Map<BitcoinAddressType, Bip32Slip10Secp256k1>>{};
Bip32Slip10Secp256k1 get mainHd => accountHD.childKey(Bip32KeyIndex(0));
@@ -372,6 +333,145 @@ abstract class ElectrumWalletBase
@observable
SyncStatus syncStatus;
+ bool _hasCompleteUnspentSet = false;
+ bool _isSyncing = false;
+
+ Future<WalletInfoAccount> loadCurrentAccount() async {
+ final accounts = await walletInfo.getAccounts();
+ final current = accounts.firstWhere(
+ (account) => account.accountIndex == currentAccountIndex,
+ orElse: () => accounts.first,
+ );
+
+ return current;
+ }
+
+ Future<List<int>> loadAccountIndexes() async {
+ if (!hasAccountsSupport) return [0];
+
+ final accounts = await walletInfo.getAccounts();
+ return accounts.map((account) => account.accountIndex).toList();
+ }
+
+ void _prepareHdForAccount(int accountIndex, CryptoCurrency? currency) {
+ // For the first account, we support all address types. For extra accounts, we only support SegwitAddresType.p2wpkh.
+ final supportedTypes =
+ accountIndex == 0 ? supportedAddressTypes(walletInfo.type) : EXTRA_ACCOUNT_ADDRESS_TYPES;
+ final isElectrumDerivation = derivationInfo.derivationType == DerivationType.electrum;
+ final canDeriveFromSeed = _masterHD != null && currency != null;
+
+ final mainMap = mainHdByTypeAndAccount[accountIndex] ??= {};
+ final sideMap = sideHdByTypeAndAccount[accountIndex] ??= {};
+
+ if (canDeriveFromSeed && !isElectrumDerivation) {
+ final coinType = _coinTypeFor(currency);
+
+ for (final type in supportedTypes) {
+ final purpose = _purposeForType(type);
+ final accountPath = "m/$purpose'/$coinType'/$accountIndex'";
+
+ mainMap[type] = _masterHD!.derivePath("$accountPath/0") as Bip32Slip10Secp256k1;
+ sideMap[type] = _masterHD!.derivePath("$accountPath/1") as Bip32Slip10Secp256k1;
+ }
+ } else {
+ // case for Electrum derivation or when we don't have seed but only xpub (view only wallet)
+ for (final type in supportedTypes) {
+ mainMap[type] = mainHd; // accountHD.child(0)
+ sideMap[type] = sideHd; // accountHD.child(1)
+ }
+ }
+ }
+
+ ElectrumBalance balanceForAccount(int accountIndex) =>
+ accountBalances[accountIndex] ?? ElectrumBalance.zero(currency);
+
+ void _updateAccountBalancesFromUnspents() {
+ if (!_hasCompleteUnspentSet) return;
+
+ final newBalances = <int, ElectrumBalance>{};
+
+ for (final coin in unspentCoins) {
+ final accountIndex = coin.bitcoinAddressRecord.accountIndex;
+ final current = newBalances[accountIndex] ?? ElectrumBalance.zero(currency);
+ final amount = Money.fromInt(coin.value, currency);
+
+ if (coin.isFrozen) {
+ current.frozen += amount;
+ }
+
+ if (coin.confirmations != null && coin.confirmations! > 0) {
+ current.confirmed += amount;
+ } else {
+ current.unconfirmed += amount;
+ }
+
+ newBalances[accountIndex] = current;
+ }
+
+ for (final accountIndex in walletAddresses.accountIndexes) {
+ newBalances.putIfAbsent(accountIndex, () => ElectrumBalance.zero(currency));
+ }
+
+ accountBalances = ObservableMap<int, ElectrumBalance>.of(newBalances);
+ _updateCurrentAccountBalance();
+ }
+
+ void _updateCurrentAccountBalance({int? accountIndex}) {
+ if (type != WalletType.bitcoin) return;
+
+ final targetAccountIndex = accountIndex ?? currentAccountIndex;
+ final newBalance = accountBalances[targetAccountIndex];
+
+ if (newBalance == null) {
+ // If the account balance is not found, set it to zero for the current currency.
+ balance[currency] = ElectrumBalance.zero(currency);
+ return;
+ }
+
+ balance[currency] = newBalance;
+ }
+
+ Map<int, Set<String>> get addressesSetByAccount {
+ final result = <int, Set<String>>{};
+ for (final addr in walletAddresses.allAddresses) {
+ if (addr.type == SegwitAddresType.mweb) continue;
+ (result[addr.accountIndex] ??= <String>{}).add(addr.address);
+ }
+ return result;
+ }
+
+ int? _accountForTransaction(
+ ElectrumTransactionBundle bundle,
+ Map<int, Set<String>> addressesByAccount,
+ ) {
+ if (type != WalletType.bitcoin) return null;
+
+ int? accountForAddress(String address) {
+ for (final entry in addressesByAccount.entries) {
+ if (entry.value.contains(address)) return entry.key;
+ }
+ return null;
+ }
+
+ for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
+ final inputTransaction = bundle.ins[i];
+ if (inputTransaction == null) continue;
+
+ final outTransaction =
+ inputTransaction.outputs[bundle.originalTransaction.inputs[i].txIndex];
+ final account =
+ accountForAddress(addressFromOutputScript(outTransaction.scriptPubKey, network));
+ if (account != null) return account;
+ }
+
+ for (final out in bundle.originalTransaction.outputs) {
+ final account = accountForAddress(addressFromOutputScript(out.scriptPubKey, network));
+ if (account != null) return account;
+ }
+
+ return null;
+ }
+
Set<String> get addressesSet => walletAddresses.allAddresses
.where((element) => element.type != SegwitAddresType.mweb)
.map((addr) => addr.address)
@@ -390,8 +490,77 @@ abstract class ElectrumWalletBase
String get xpub => accountHD.publicKey.toExtended;
+ int get currentAccountIndex => walletInfo.currentAccountIndex;
+
+ @action
+ Future<void> setCurrentAccount(int accountIndex) async {
+
+ final isNewAccount = !walletAddresses.accountIndexes.contains(accountIndex);
+ if (isNewAccount) {
+ walletAddresses.accountIndexes.add(accountIndex);
+
+ accountBalances[accountIndex] = ElectrumBalance.zero(currency);
+ }
+
+ await walletInfo.setSelectedAccount(accountIndex);
+
+ _prepareHdForAccount(accountIndex, currency);
+
+ walletAddresses.currentAccountIndex = accountIndex;
+
+ final currentPageType = walletAddresses.addressPageType;
+ await walletAddresses.setAddressType(currentPageType);
+
+ // For extra accounts, we only prepare SegwitAddresType.p2wpkh type.
+ if (isNewAccount) {
+ await walletAddresses.prepareAccountAddresses(
+ accountIndex,
+ types: accountIndex == 0 ? BITCOIN_ADDRESS_TYPES : EXTRA_ACCOUNT_ADDRESS_TYPES,
+ );
+ await walletAddresses.updateAddressesInBox();
+ }
+
+ walletAddresses.updateReceiveAddresses();
+ walletAddresses.updateChangeAddresses();
+
+ _updateCurrentAccountBalance(accountIndex: accountIndex);
+
+ unawaited(() async {
+ try {
+ if (isNewAccount && !_isSyncing) {
+ final histories = <String, ElectrumTransactionInfo>{};
+
+ final typesToFetch = accountIndex == 0 ? BITCOIN_ADDRESS_TYPES : EXTRA_ACCOUNT_ADDRESS_TYPES;
+ // Explicit for clarity — fetchTransactionsForAddressTypeBatch already skips types
+ // with no addresses for this account, so this doesn't change RPC calls, just intent.
+
+ for (final addressType in typesToFetch) {
+ if (shouldUseBatchFetching) {
+ await fetchTransactionsForAddressTypeBatch(histories, addressType,
+ accountIndex: accountIndex);
+ } else {
+ await fetchTransactionsForAddressType(histories, addressType,
+ accountIndex: accountIndex);
+ }
+ }
+
+ await resolveQueuedTransactionDetails();
+ }
+
+ await updateAllUnspents();
+ } catch (e) {
+ printV('setCurrentAccount balance update failed: $e');
+ }
+ }());
+ }
+
bool get shouldUseBatchFetching => useBatchForHistory && _isBatchSupported == true;
+ bool get isInitialBitcoinAccountsSync =>
+ hasAccountsSupport &&
+ walletInfo.isMultiAccountsEnabled == true &&
+ (walletInfo.accountDiscoveryLimit ?? 0) < maxProbAccounts;
+
@override
String? get seed => _mnemonic;
@@ -410,6 +579,13 @@ abstract class ElectrumWalletBase
@override
bool get hasSilentPaymentsScanning => type == WalletType.bitcoin && keys.privateKey.isNotEmpty;
+ @override
+ bool get hasAccountsSupport =>
+ type == WalletType.bitcoin &&
+ isSoftwareWallet &&
+ _masterHD != null && // xpub / watch-only: can't derive other (hardened) accounts
+ derivationInfo.derivationType != DerivationType.electrum;
+
@observable
bool nodeSupportsSilentPayments = true;
@observable
@@ -418,6 +594,9 @@ abstract class ElectrumWalletBase
bool _isTryingToConnect = false;
bool? _isBatchSupported;
DateTime? _syncBenchmarkStartTime;
+ DateTime? _syncStartedAt;
+
+ static const Duration maxSyncDuration = Duration(minutes: 2);
Completer<SharedPreferences> sharedPrefs = Completer();
@@ -449,7 +628,7 @@ abstract class ElectrumWalletBase
_isolate?.then((value) => value.kill(priority: Isolate.immediate));
if (electrumClient.isConnected) {
- syncStatus = SyncedSyncStatus();
+ if (!_isSyncing) syncStatus = SyncedSyncStatus();
} else {
syncStatus = NotConnectedSyncStatus();
}
@@ -490,7 +669,9 @@ abstract class ElectrumWalletBase
String? privateKey;
String? publicKey;
- final hd = mainHdByType[SegwitAddresType.p2wpkh] ?? mainHd;
+ // `keys` is wallet-level metadata
+ final hd =
+ mainHdByTypeAndAccount[0]?[SegwitAddresType.p2wpkh] ?? mainHd;
try {
wif = WifEncoder.encode(hd.privateKey.raw, netVer: network.wifNetVer);
@@ -523,16 +704,29 @@ abstract class ElectrumWalletBase
bool _isTransactionUpdating;
Future<Isolate>? _isolate;
+ final Map<String, Map<String, dynamic>> _missingHistoryQueue = {};
+
+ Future<void>? _resolveMissingHistoryFuture;
+ final Map<String, int> _unresolvedAccountRetryCount = {};
+ static const int _maxUnresolvedAccountRetries = 3;
+
void Function(FlutterErrorDetails)? _onError;
Timer? _autoSaveTimer;
StreamSubscription<dynamic>? _receiveStream;
Timer? _updateFeeRateTimer;
static const int _autoSaveInterval = 1;
Future<void> init() async {
- await walletAddresses.init();
+ final accountIndexes = await loadAccountIndexes();
+
+ for (final accountIndex in accountIndexes) {
+ _prepareHdForAccount(accountIndex, currency);
+ }
+
+ await walletAddresses.init(accountIndexes: accountIndexes);
await transactionHistory.init();
await cleanUpDuplicateUnspentCoins();
+ _seedAccountBalancesFromAddressRecords();
await save();
_autoSaveTimer =
@@ -699,14 +893,32 @@ abstract class ElectrumWalletBase
DateTime? _lastSilentPaymentsScan;
static const Duration _silentPaymentsScanDelay = Duration(minutes: 1);
+ Iterable<BitcoinUnspent> get unspentCoinsForCurrentAccount {
+ if (type != WalletType.bitcoin) {
+ return unspentCoins;
+ }
+
+ return unspentCoins
+ .where((coin) => coin.bitcoinAddressRecord.accountIndex == currentAccountIndex);
+ }
+
@action
@override
Future<void> startSync() async {
- try {
- if (syncStatus is SyncronizingSyncStatus) {
+ if (_isSyncing) {
+ final startedAt = _syncStartedAt;
+ if (startedAt != null && DateTime.now().difference(startedAt) > maxSyncDuration) {
+ printV('startSync: previous sync appears stuck, allowing a new one');
+ _isSyncing = false;
+ } else {
+ printV('startSync: already syncing, skipping');
return;
}
+ }
+ _isSyncing = true;
+ _syncStartedAt = DateTime.now();
+ try {
if (_syncBenchmarkStartTime == null) {
_syncBenchmarkStartTime = DateTime.now();
printV('[ELECTRUM_WALLET SYNC] Starting: ${_syncBenchmarkStartTime!}');
@@ -749,18 +961,12 @@ abstract class ElectrumWalletBase
await updateAllUnspents();
await updateBalance();
- await updateFeeRates();
-
- _updateFeeRateTimer ??=
- Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates());
if (alwaysScan == true) {
setSilentPaymentsScanning(true);
+ } else if (syncStatus is LostConnectionSyncStatus) {
+ _syncBenchmarkStartTime = null;
} else {
- if (syncStatus is LostConnectionSyncStatus) {
- return;
- }
-
final syncEnd = DateTime.now();
final totalMs = _syncBenchmarkStartTime != null
? syncEnd.difference(_syncBenchmarkStartTime!).inMilliseconds
@@ -771,6 +977,13 @@ abstract class ElectrumWalletBase
_syncBenchmarkStartTime = null;
syncStatus = SyncedSyncStatus();
}
+
+ await updateFeeRates();
+
+ _updateFeeRateTimer ??=
+ Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates());
+
+ unawaited(resolveQueuedTransactionDetails());
} catch (e, stacktrace) {
final syncEnd = DateTime.now();
final totalMs = _syncBenchmarkStartTime != null
@@ -783,6 +996,8 @@ abstract class ElectrumWalletBase
_syncBenchmarkStartTime = null;
syncStatus = FailedSyncStatus();
+ } finally {
+ _isSyncing = false;
}
}
@@ -939,7 +1154,7 @@ abstract class ElectrumWalletBase
bool spendsUnconfirmedTX = false;
int leftAmount = credentialsAmount;
- var availableInputs = unspentCoins.where((utx) {
+ var availableInputs = unspentCoinsForCurrentAccount.where((utx) {
if (!utx.isSending || utx.isFrozen) {
return false;
}
@@ -1020,7 +1235,7 @@ abstract class ElectrumWalletBase
final address = RegexUtils.addressTypeFromStr(utx.address, network);
ECPrivate? privkey;
bool? isSilentPayment = false;
- final hd = _hdFor(record: utx.bitcoinAddressRecord);
+ final hd = _hdForAddressRecord(record: utx.bitcoinAddressRecord);
if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord;
@@ -1287,7 +1502,7 @@ abstract class ElectrumWalletBase
final changeDerivationPath = "${_hardenedDerivationPath(changeBaseDerivationPath)}"
"/${changeAddress.isHidden ? "1" : "0"}"
"/${changeAddress.index}";
- final changeHd = _hdFor(record: changeAddress).childKey(Bip32KeyIndex(changeAddress.index));
+ final changeHd = _hdForAddressRecord(record: changeAddress).childKey(Bip32KeyIndex(changeAddress.index));
final changePubKeyHex = changeHd.publicKey.toHex();
utxoDetails.publicKeys[address.pubKeyHash()] =
@@ -1617,6 +1832,7 @@ abstract class ElectrumWalletBase
return PendingBitcoinTransaction(
transaction,
type,
+ accountIndex: currentAccountIndex,
electrumClient: electrumClient,
amount: estimatedTx.amount,
fee: estimatedTx.fee,
@@ -1708,6 +1924,7 @@ abstract class ElectrumWalletBase
return PendingBitcoinTransaction(
transaction,
type,
+ accountIndex: currentAccountIndex,
electrumClient: electrumClient,
amount: estimatedTx.amount,
fee: estimatedTx.fee,
@@ -1819,7 +2036,7 @@ abstract class ElectrumWalletBase
if (amount != null) {
int totalValue = 0;
- for (final input in unspentCoins) {
+ for (final input in unspentCoinsForCurrentAccount) {
if (totalValue >= amount) {
break;
}
@@ -1832,7 +2049,7 @@ abstract class ElectrumWalletBase
if (totalValue < amount) return 0;
} else {
- for (final input in unspentCoins) {
+ for (final input in unspentCoinsForCurrentAccount) {
if (input.isSending) {
inputsCount += 1;
}
@@ -1875,6 +2092,8 @@ abstract class ElectrumWalletBase
@override
Future<void> close({bool shouldCleanup = false}) async {
+ _missingHistoryQueue.clear();
+ _unresolvedAccountRetryCount.clear();
try {
await _receiveStream?.cancel();
await electrumClient.close();
@@ -1886,6 +2105,7 @@ abstract class ElectrumWalletBase
@action
Future<void> updateAllUnspents() async {
+
List<BitcoinUnspent> updatedUnspentCoins = [];
final previousUnspentCoins = List<BitcoinUnspent>.from(unspentCoins.where((utxo) =>
@@ -1910,6 +2130,7 @@ abstract class ElectrumWalletBase
for (final addr in targetAddresses) {
if (addr is! BitcoinSilentPaymentAddressRecord) {
addr.balance = 0;
+ _appliedCoinKeysByAddress[addr.address]?.clear();
}
}
@@ -1924,6 +2145,7 @@ abstract class ElectrumWalletBase
updatedUnspentCoins.addAll(result!);
}
unspentCoins = updatedUnspentCoins;
+ _hasCompleteUnspentSet = true;
} else {
if (updatedUnspentCoins.isEmpty) {
unspentCoins = handleFailedUtxoFetch(
@@ -1945,6 +2167,7 @@ abstract class ElectrumWalletBase
}
await updateCoins(unspentCoins);
+ _updateAccountBalancesFromUnspents();
await _refreshUnspentCoinsInfo();
}
@@ -1965,11 +2188,13 @@ abstract class ElectrumWalletBase
final scriptHashes = byScriptHash.keys.toList();
try {
+ final failedScriptHashes = <String>{};
final unspentByScriptHash =
await _processChunksToMap<String, String, List<Map<String, dynamic>>>(
items: scriptHashes,
chunkSize: addressHistoryChunkSize,
processChunk: _getListUnspentBatch,
+ onChunkError: (chunk, _) => failedScriptHashes.addAll(chunk),
);
final txHashes = <String>{};
@@ -2007,6 +2232,8 @@ abstract class ElectrumWalletBase
return addresses.map((address) {
final scriptHash = address.getScriptHash(network);
+ if (failedScriptHashes.contains(scriptHash)) return null;
+ if (!unspentByScriptHash.containsKey(scriptHash)) return null;
return coinsByScriptHash[scriptHash] ?? <BitcoinUnspent>[];
}).toList();
} catch (e) {
@@ -2066,8 +2293,15 @@ abstract class ElectrumWalletBase
coin.isSending = coinInfo.isSending;
coin.note = coinInfo.note;
- if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
- coin.bitcoinAddressRecord.balance += coinInfo.value;
+ if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) {
+ final addr = coin.bitcoinAddressRecord.address;
+ final coinKey = '${coin.hash}:${coin.vout}';
+ final appliedKeys = _appliedCoinKeysByAddress.putIfAbsent(addr, () => <String>{});
+
+ if (appliedKeys.add(coinKey)) {
+ coin.bitcoinAddressRecord.balance += coinInfo.value;
+ }
+ }
} else {
addCoinInfo(coin);
}
@@ -2175,6 +2409,41 @@ abstract class ElectrumWalletBase
if (duplicateKeys.isNotEmpty) await unspentCoinsInfo.deleteAll(duplicateKeys);
}
+ void _seedAccountBalancesFromAddressRecords() {
+ if (type != WalletType.bitcoin) return;
+ if (accountBalances.isNotEmpty) return;
+
+ final newBalances = <int, ElectrumBalance>{};
+
+ for (final addr in walletAddresses.allAddresses) {
+ if (addr.type == SegwitAddresType.mweb) continue;
+ final current = newBalances[addr.accountIndex] ?? ElectrumBalance.zero(currency);
+ current.confirmed += Money.fromInt(addr.balance, currency);
+ newBalances[addr.accountIndex] = current;
+ }
+
+ for (final accountIndex in walletAddresses.accountIndexes) {
+ newBalances.putIfAbsent(accountIndex, () => ElectrumBalance.zero(currency));
+ }
+
+ // balance[currency] came from the wallet file and is authoritative for the
+ // selected account — prefer it if the address records look stale.
+ final persisted = balance[currency];
+ final seeded = newBalances[currentAccountIndex];
+ if (persisted != null && seeded != null) {
+ final seededIsZero = seeded.confirmed == Money.zero(currency) &&
+ seeded.unconfirmed == Money.zero(currency);
+ final persistedIsNonZero = persisted.confirmed != Money.zero(currency) ||
+ persisted.unconfirmed != Money.zero(currency);
+ if (seededIsZero && persistedIsNonZero) {
+ newBalances[currentAccountIndex] = persisted;
+ }
+ }
+
+ accountBalances = ObservableMap<int, ElectrumBalance>.of(newBalances);
+ _updateCurrentAccountBalance();
+ }
+
int transactionVSize(String transactionHex) => BtcTransaction.fromRaw(transactionHex).getVSize();
Future<String?> canReplaceByFee(ElectrumTransactionInfo tx) async {
@@ -2203,7 +2472,8 @@ abstract class ElectrumWalletBase
throw Exception("Receiver output not found.");
}
- final availableInputs = unspentCoins.where((utxo) => utxo.isSending && !utxo.isFrozen).toList();
+ final availableInputs =
+ unspentCoinsForCurrentAccount.where((utxo) => utxo.isSending && !utxo.isFrozen).toList();
int totalBalance = availableInputs.fold<int>(
0, (previousValue, element) => previousValue + element.value.toInt());
@@ -2254,7 +2524,7 @@ abstract class ElectrumWalletBase
walletAddresses.allAddresses.firstWhere((element) => element.address == address);
final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network);
- final hd = _hdFor(record: addressRecord);
+ final hd = _hdForAddressRecord(record: addressRecord);
final privkey = generateECPrivate(hd: hd, index: addressRecord.index, network: network);
@@ -2331,15 +2601,15 @@ abstract class ElectrumWalletBase
// If still not enough, add UTXOs until the fee is covered, drawing them at
// random instead of in the predictable wallet scan order (address, then age).
if (remainingFee > BigInt.zero) {
- final unusedUtxos = unspentCoins
+ final unusedUtxos = unspentCoinsForCurrentAccount
.where((utxo) => utxo.isSending && !utxo.isFrozen && utxo.confirmations! > 0)
.toList()
..shuffle(Random.secure());
for (final utxo in unusedUtxos) {
final address = RegexUtils.addressTypeFromStr(utxo.address, network);
- final hd = _hdFor(record: utxo.bitcoinAddressRecord);
+ final hd = _hdForAddressRecord(record: utxo.bitcoinAddressRecord);
final privkey = generateECPrivate(
hd: hd,
@@ -2449,6 +2719,7 @@ abstract class ElectrumWalletBase
return PendingBitcoinTransaction(
transaction,
type,
+ accountIndex: transactionHistory.transactions[hash]?.accountIndex ?? currentAccountIndex,
electrumClient: electrumClient,
amount: Money.fromInt(sendingAmount, currency),
fee: Money.fromInt(newFee, currency),
@@ -2457,17 +2728,17 @@ abstract class ElectrumWalletBase
feeRate: newFee.toString(),
isViewOnly: keys.privateKey.isEmpty,
)..addListener((transaction) async {
- transactionHistory.transactions.values.forEach((tx) {
- if (tx.id == hash) {
- tx.isReplaced = true;
- tx.isPending = false;
- transactionHistory.addOne(tx);
- }
- });
- transactionHistory.addOne(transaction);
- await updateBalance();
- await updateAllUnspents();
+ transactionHistory.transactions.values.forEach((tx) {
+ if (tx.id == hash) {
+ tx.isReplaced = true;
+ tx.isPending = false;
+ transactionHistory.addOne(tx);
+ }
});
+ transactionHistory.addOne(transaction);
+ await updateBalance();
+ await updateAllUnspents();
+ });
} catch (e) {
throw e;
}
@@ -2564,13 +2835,19 @@ abstract class ElectrumWalletBase
Future<ElectrumTransactionInfo?> fetchTransactionInfo(
{required String hash, int? height, bool? retryOnFailure}) async {
try {
- return ElectrumTransactionInfo.fromElectrumBundle(
- await getTransactionExpanded(hash: hash, height: height),
+ final bundle = await getTransactionExpanded(hash: hash, height: height);
+ final addressesByAccount = addressesSetByAccount;
+ final owner = _accountForTransaction(bundle, addressesByAccount);
+
+ final info = ElectrumTransactionInfo.fromElectrumBundle(
+ bundle,
walletInfo.type,
network,
- addresses: addressesSet,
+ addresses: owner != null ? addressesByAccount[owner]! : addressesSet,
height: height,
);
+ info.accountIndex = owner;
+ return info;
} catch (e) {
if (e is FormatException && retryOnFailure == true) {
await Future.delayed(const Duration(seconds: 2));
@@ -2589,14 +2866,17 @@ abstract class ElectrumWalletBase
Future<Map<String, ElectrumTransactionInfo>> fetchTransactions() async {
try {
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
- ;
- printV('[BATCH_TEST] Fetching transactions with batch: $shouldUseBatchFetching');
+ printV("[BATCH_TEST] Fetching transactions with batch: $shouldUseBatchFetching");
if (type == WalletType.bitcoin) {
- await Future.wait(BITCOIN_ADDRESS_TYPES.map((type) => shouldUseBatchFetching
- ? fetchTransactionsForAddressTypeBatch(historiesWithDetails, type)
- : fetchTransactionsForAddressType(historiesWithDetails, type)));
+ if (isInitialBitcoinAccountsSync) {
+ await fetchBitcoinTransactionsForInitialRestore(historiesWithDetails);
+ } else {
+ await Future.wait(BITCOIN_ADDRESS_TYPES.map((type) => shouldUseBatchFetching
+ ? fetchTransactionsForAddressTypeBatch(historiesWithDetails, type)
+ : fetchTransactionsForAddressType(historiesWithDetails, type)));
+ }
} else if (type == WalletType.bitcoinCash) {
await Future.wait(BITCOIN_CASH_ADDRESS_TYPES.map((type) => shouldUseBatchFetching
? fetchTransactionsForAddressTypeBatch(historiesWithDetails, type)
@@ -2612,9 +2892,12 @@ abstract class ElectrumWalletBase
: fetchTransactionsForAddressType(historiesWithDetails, type)));
}
+ printV('[QUICK_SYNC] deferred ${_missingHistoryQueue.length} transactions to background');
+
transactionHistory.transactions.values.forEach((tx) async {
- final isPendingSilentPaymentUtxo =
- (tx.isPending || tx.confirmations == 0) && historiesWithDetails[tx.id] == null;
+ final isPendingSilentPaymentUtxo = (tx.isPending || tx.confirmations == 0) &&
+ historiesWithDetails[tx.id] == null &&
+ !_missingHistoryQueue.containsKey(tx.id);
if (isPendingSilentPaymentUtxo) {
final info =
@@ -2637,293 +2920,190 @@ abstract class ElectrumWalletBase
}
Future<void> fetchTransactionsForAddressType(
- Map<String, ElectrumTransactionInfo> historiesWithDetails,
- BitcoinAddressType type,
- ) async {
- final addressesByType =
- walletAddresses.allAddresses.where((addr) => addr.type == type).toList();
+ Map<String, ElectrumTransactionInfo> historiesWithDetails,
+ BitcoinAddressType type, {
+ int? accountIndex,
+ }) async {
+ final addressesByType = walletAddresses.allAddresses
+ .where((addr) => addr.type == type)
+ .where((addr) => accountIndex == null || addr.accountIndex == accountIndex)
+ .toList();
+
- final receiveStandard = getAddressBranchByType(hidden: false, legacy: false, type: type);
- final changeStandard = getAddressBranchByType(hidden: true, legacy: false, type: type);
- final receiveLegacy = getAddressBranchByType(hidden: false, legacy: true, type: type);
- final changeLegacy = getAddressBranchByType(hidden: true, legacy: true, type: type);
+ final branchCache = <String, List<BitcoinAddressRecord>>{};
+ List<BitcoinAddressRecord> branchFor(BitcoinAddressRecord record) =>
+ branchCache.putIfAbsent(
+ '${record.accountIndex}:${record.isHidden}:${record.isLegacyDerivation}',
+ () => getAddressBranchByType(
+ type: type,
+ hidden: record.isHidden,
+ legacy: record.isLegacyDerivation,
+ accountIndex: record.accountIndex,
+ ),
+ );
walletAddresses.hiddenAddresses
- .addAll([...changeStandard, ...changeLegacy].map((e) => e.address));
+ .addAll(addressesByType.where((addr) => addr.isHidden).map((e) => e.address));
await walletAddresses.saveAddressesInBox();
+
await Future.wait(addressesByType.map((addressRecord) async {
- final history = await _fetchAddressHistory(addressRecord, await getCurrentChainTip());
+ final result = await _fetchAddressHistory(addressRecord, await getCurrentChainTip());
- if (history.isNotEmpty) {
- addressRecord.txCount = history.length;
- historiesWithDetails.addAll(history);
+ if (result.hasHistory) {
+ historiesWithDetails.addAll(result.transactions);
- final matchedAddresses = addressRecord.isHidden
- ? (addressRecord.isLegacyDerivation ? changeLegacy : changeStandard)
- : (addressRecord.isLegacyDerivation ? receiveLegacy : receiveStandard);
- final isUsedAddressAboveGap = matchedAddresses.toList().indexOf(addressRecord) >=
+ final matchedAddresses = branchFor(addressRecord);
+ final isUsedAddressAboveGap = matchedAddresses.indexOf(addressRecord) >=
matchedAddresses.length -
(addressRecord.isHidden
? ElectrumWalletAddressesBase.defaultChangeAddressesCount
: ElectrumWalletAddressesBase.defaultReceiveAddressesCount);
if (isUsedAddressAboveGap) {
- final prevLength = walletAddresses.allAddresses.length;
// Discover new addresses for the same address type until the gap limit is respected
await walletAddresses.discoverAddresses(
- matchedAddresses.toList(),
+ matchedAddresses,
addressRecord.isHidden,
- (address) async {
+ (address) async {
await subscribeForUpdates();
return _fetchAddressHistory(address, await getCurrentChainTip())
- .then((history) => history.isNotEmpty ? address.address : null);
+ .then((result) => result.hasHistory ? address.address : null);
},
type: type,
isLegacyDerivation: addressRecord.isLegacyDerivation,
+ accountIndex: addressRecord.accountIndex,
);
- final newLength = walletAddresses.allAddresses.length;
-
- if (newLength > prevLength) {
- await fetchTransactionsForAddressType(historiesWithDetails, type);
- }
}
}
- }));
+ }),);
}
- Future<Map<String, ElectrumTransactionInfo>> _fetchAddressHistory(
- BitcoinAddressRecord addressRecord, int? currentHeight) async {
- String txid = "";
-
- try {
- final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
-
- final history = await electrumClient.getHistory(addressRecord.getScriptHash(network));
+ Future<AddressHistoryResult> _fetchAddressHistory(
+ BitcoinAddressRecord addressRecord, int? currentHeight) =>
+ _fetchAddressHistories([addressRecord], currentHeight, addressHistoryChunkSize);
- if (history.isNotEmpty) {
- addressRecord.setAsUsed();
- walletAddresses.clearLockIfMatches(addressRecord.type, addressRecord.address);
+ Future<AddressHistoryResult> _fetchAddressHistories(
+ List<BitcoinAddressRecord> addressRecords,
+ int? currentHeight,
+ int historyChunkSize) async {
+ final scan = await _scanAddressHistories(addressRecords, historyChunkSize);
- if (this is BitcoinWallet) {
- //removes transactions no longer returned by the api, presumed replaced/invalid.
- transactionHistory.transactions.removeWhere(
- (hash, tx) =>
- tx.outputAddresses != null &&
- tx.outputAddresses!.contains(addressRecord.address) &&
- !history.any((newTransaction) => newTransaction['tx_hash'] == hash),
- );
- }
+ final split = await _markUsedAndSplitHistory(scan, addressRecords, currentHeight);
- await Future.wait(history.map((transaction) async {
- txid = transaction['tx_hash'] as String;
- final height = transaction['height'] as int;
- final storedTx = transactionHistory.transactions[txid];
+ _enqueueMissingHistory(split.missingHistoryItems);
- if (storedTx != null) {
- if (height > 0) {
- storedTx.height = height;
- // the tx's block itself is the first confirmation so add 1
- if ((currentHeight ?? 0) > 0) {
- storedTx.confirmations = currentHeight! - height + 1;
- }
- storedTx.isPending = storedTx.confirmations == 0;
- }
+ return split;
+ }
- historiesWithDetails[txid] = storedTx;
- } else {
- final tx = await fetchTransactionInfo(hash: txid, height: height, retryOnFailure: true);
+ void _enqueueMissingHistory(List<Map<String, dynamic>> missingHistoryItems) {
+ for (final item in missingHistoryItems) {
+ final txid = item['tx_hash'] as String?;
+ if (txid == null || txid.isEmpty) continue;
+ _missingHistoryQueue.putIfAbsent(txid, () => item);
+ }
+ }
+
+ Future<void> resolveQueuedTransactionDetails() {
+ final inProgress = _resolveMissingHistoryFuture;
+ if (inProgress != null) return inProgress;
- if (tx != null) {
- historiesWithDetails[txid] = tx;
+ if (_missingHistoryQueue.isEmpty) return Future.value();
- // Got a new transaction fetched, add it to the transaction history
- // instead of waiting all to finish, and next time it will be faster
+ final future = _drainMissingHistoryQueue();
+ _resolveMissingHistoryFuture = future;
+ return future.whenComplete(() => _resolveMissingHistoryFuture = null);
+ }
- _applyLitecoinPegOutTag(tx);
- transactionHistory.addOne(tx);
- await transactionHistory.save();
- }
- }
+ Future<void> _drainMissingHistoryQueue() async {
+ final watch = Stopwatch()..start();
+ var resolvedCount = 0;
+ try {
+ printV('[QUICK_SYNC] resolving ${_missingHistoryQueue.length} deferred transactions');
- return Future.value(null);
- }));
+ while (_missingHistoryQueue.isNotEmpty) {
+ final queued = _missingHistoryQueue.values.toList(growable: false);
+ _missingHistoryQueue.clear();
+ final resolved = await _resolveTransactions(queued, addressHistoryChunkSize);
+ resolvedCount += resolved.length;
}
-
- return historiesWithDetails;
- } catch (e, stacktrace) {
- _onError?.call(FlutterErrorDetails(
- exception: "$txid - $e",
- stack: stacktrace,
- library: this.runtimeType.toString(),
- ));
- return {};
+ } finally {
+ printV(
+ '[QUICK_SYNC] resolved $resolvedCount deferred transactions in ${watch.elapsedMilliseconds} ms');
}
}
- Future<void> fetchTransactionsForAddressTypeBatch(
- Map<String, ElectrumTransactionInfo> historiesWithDetails, BitcoinAddressType type) async {
- final receiveStandard = getAddressBranchByType(hidden: false, legacy: false, type: type);
- final changeStandard = getAddressBranchByType(hidden: true, legacy: false, type: type);
- final receiveLegacy = getAddressBranchByType(hidden: false, legacy: true, type: type);
- final changeLegacy = getAddressBranchByType(hidden: true, legacy: true, type: type);
+ Future<AddressHistoryScan> _scanAddressHistories(
+ List<BitcoinAddressRecord> addressRecords, int historyChunkSize) async {
+ final historyByScriptHash = <String, List<Map<String, dynamic>>>{};
+ var hadFailures = false;
- walletAddresses.hiddenAddresses
- .addAll([...changeStandard, ...changeLegacy].map((e) => e.address));
- await walletAddresses.saveAddressesInBox();
+ if (shouldUseBatchFetching) {
+ final scriptHashes = addressRecords.map((a) => a.getScriptHash(network)).toList();
- await fetchTransactionsForAddressesBranchBatch(
- historiesWithDetails,
- type,
- receiveStandard,
- isHidden: false,
- isLegacyDerivation: false,
- );
+ historyByScriptHash.addAll(
+ await _processChunksToMap<String, String, List<Map<String, dynamic>>>(
+ items: scriptHashes,
+ chunkSize: historyChunkSize,
+ processChunk: _getHistoryBatch,
+ onChunkError: (chunk, error) {
+ hadFailures = true;
+ printV('address history scan failed for ${chunk.length} scripthashes: $error');
+ },
+ ),
+ );
- await fetchTransactionsForAddressesBranchBatch(
- historiesWithDetails,
- type,
- changeStandard,
- isHidden: true,
- isLegacyDerivation: false,
- );
+ return AddressHistoryScan(
+ historyByScriptHash: historyByScriptHash,
+ hadFailures: hadFailures,
+ );
+ }
- await fetchTransactionsForAddressesBranchBatch(
- historiesWithDetails,
- type,
- receiveLegacy,
- isHidden: false,
- isLegacyDerivation: true,
- );
+ await Future.wait(addressRecords.map((addressRecord) async {
+ final scriptHash = addressRecord.getScriptHash(network);
+
+ try {
+ historyByScriptHash[scriptHash] = await electrumClient.getHistory(scriptHash);
+ } catch (e) {
+ hadFailures = true;
+ printV('address history scan failed for ${addressRecord.address}: $e');
+ }
+ }));
- await fetchTransactionsForAddressesBranchBatch(
- historiesWithDetails,
- type,
- changeLegacy,
- isHidden: true,
- isLegacyDerivation: true,
+ return AddressHistoryScan(
+ historyByScriptHash: historyByScriptHash,
+ hadFailures: hadFailures,
);
}
- Future<void> fetchTransactionsForAddressesBranchBatch(
- Map<String, ElectrumTransactionInfo> historiesWithDetails,
- BitcoinAddressType type,
- List<BitcoinAddressRecord> branchAddresses, {
- required bool isHidden,
- required bool isLegacyDerivation,
- }) async {
- if (branchAddresses.isEmpty) return;
+ Future<AddressHistoryResult> _markUsedAndSplitHistory(AddressHistoryScan scan,
+ List<BitcoinAddressRecord> addressRecords, int? currentHeight) async {
- final tip = await getCurrentChainTip();
- final currentBranch = [...branchAddresses];
-
- final initialHistory =
- await _processChunksToMap<BitcoinAddressRecord, String, ElectrumTransactionInfo>(
- items: currentBranch,
- chunkSize: addressHistoryChunkSize,
- processChunk: (chunk) => _fetchBatchAddressHistory(
- chunk,
- tip,
- addressHistoryChunkSize,
- ),
- );
-
- if (initialHistory.isNotEmpty) {
- historiesWithDetails.addAll(initialHistory);
- }
-
- final gapLimit = isHidden
- ? ElectrumWalletAddressesBase.defaultChangeAddressesCount
- : ElectrumWalletAddressesBase.defaultReceiveAddressesCount;
-
- final highestUsedIndex = _highestUsedIndex(currentBranch);
- final shouldDiscover =
- highestUsedIndex >= 0 && highestUsedIndex >= currentBranch.length - gapLimit;
-
- if (!shouldDiscover) return;
-
- final newAddresses = await walletAddresses.discoverAddressesBatch(
- currentBranch,
- isHidden,
- (newAddresses) async {
- final newHistory = await _fetchBatchAddressHistory(
- newAddresses,
- tip,
- discoveryHistoryChunkSize,
- );
-
- if (newHistory.isNotEmpty) {
- historiesWithDetails.addAll(newHistory);
- }
-
- return newAddresses
- .where((addressRecord) => addressRecord.isUsed)
- .map((addressRecord) => addressRecord.address)
- .toSet();
- },
- type: type,
- isLegacyDerivation: isLegacyDerivation,
- );
-
- if (newAddresses.isNotEmpty) {
- currentBranch.addAll(newAddresses);
-
- if (isHidden) {
- walletAddresses.hiddenAddresses.addAll(newAddresses.map((e) => e.address));
- await walletAddresses.saveAddressesInBox();
- }
- }
- }
-
- List<BitcoinAddressRecord> getAddressBranchByType(
- {required bool hidden, required bool legacy, required BitcoinAddressType type}) =>
- walletAddresses.allAddresses
- .where((addr) =>
- addr.type == type && addr.isHidden == hidden && addr.isLegacyDerivation == legacy)
- .toList()
- ..sort((a, b) => a.index.compareTo(b.index));
-
- int _highestUsedIndex(List<BitcoinAddressRecord> addresses) {
- for (int i = addresses.length - 1; i >= 0; i--) {
- if (addresses[i].isUsed) return i;
- }
- return -1;
- }
-
- Future<Map<String, ElectrumTransactionInfo>> _fetchBatchAddressHistory(
- List<BitcoinAddressRecord> addressRecords, int? currentHeight, int historyChunkSize) async {
String lastTxId = '';
bool didUpdateHistory = false;
try {
- final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
-
- // List of script hashes for the given address records
- final scriptHashes = addressRecords.map((a) => a.getScriptHash(network)).toList();
-
- final historyByScriptHash =
- await _processChunksToMap<String, String, List<Map<String, dynamic>>>(
- items: scriptHashes, chunkSize: historyChunkSize, processChunk: _getHistoryBatch);
+ final Map<String, ElectrumTransactionInfo> transactions = {};
+ final missingHistoryItems = <Map<String, dynamic>>[];
+ var hasHistory = false;
- // Map scriptHash -> addressRecord
final byScriptHash = <String, BitcoinAddressRecord>{};
for (final a in addressRecords) {
byScriptHash[a.getScriptHash(network)] = a;
}
- // Split into already-known txs vs missing txs
- final missingHistoryItems = <Map<String, dynamic>>[];
-
- for (final entry in historyByScriptHash.entries) {
+ for (final entry in scan.historyByScriptHash.entries) {
final sh = entry.key;
final addressRecord = byScriptHash[sh];
if (addressRecord == null) continue;
final history = entry.value;
if (history.isEmpty) continue;
+ hasHistory = true;
addressRecord.setAsUsed();
+ addressRecord.txCount = history.length;
walletAddresses.clearLockIfMatches(addressRecord.type, addressRecord.address);
//removes transactions no longer returned by the api, presumed replaced/invalid.
@@ -2939,7 +3119,6 @@ abstract class ElectrumWalletBase
}
}
- // For each transaction in the history, check if we already have it in our transaction history. If we do, update its details if necessary. If we don't, add it to the list of missing history items to fetch later.
for (final item in history) {
final txid = item['tx_hash'] as String?;
final height = item['height'] as int? ?? 0;
@@ -2948,14 +3127,27 @@ abstract class ElectrumWalletBase
lastTxId = txid;
final storedTx = transactionHistory.transactions[txid];
- if (storedTx != null) {
+ // If we've already tried resolving this tx's account owner
+ // several times and it still can't be determined, stop treating it
+ // as "missing" every scan - otherwise we'd refetch its full raw tx
+ // (and all its inputs' raw txs) forever with no chance of ever
+ // converging.
+ final hasExhaustedAccountRetries =
+ (_unresolvedAccountRetryCount[txid] ?? 0) >= _maxUnresolvedAccountRetries;
+ final needsAccount = type == WalletType.bitcoin &&
+ storedTx != null &&
+ storedTx.accountIndex == null &&
+ !hasExhaustedAccountRetries;
+
+ if (storedTx != null && !needsAccount) {
if (height > 0) {
final oldHeight = storedTx.height;
final oldConfs = storedTx.confirmations;
final oldPending = storedTx.isPending;
storedTx.height = height;
+ // the tx's block itself is the first confirmation so add 1
if ((currentHeight ?? 0) > 0) {
storedTx.confirmations = currentHeight! - height + 1;
}
@@ -2970,7 +3162,7 @@ abstract class ElectrumWalletBase
}
}
- historiesWithDetails[txid] = storedTx;
+ transactions[txid] = storedTx;
} else {
missingHistoryItems.add({
'tx_hash': txid,
@@ -2982,12 +3174,86 @@ abstract class ElectrumWalletBase
}
}
- // Batch fetch missing tx verbose details
- if (missingHistoryItems.isEmpty) {
- if (didUpdateHistory) await transactionHistory.save();
- return historiesWithDetails;
+ if (didUpdateHistory) {
+ await transactionHistory.save();
}
+ return AddressHistoryResult(
+ transactions: transactions,
+ missingHistoryItems: missingHistoryItems,
+ hasHistory: hasHistory,
+ hadFailures: scan.hadFailures,
+ );
+ } catch (e, stacktrace) {
+ final prefix = lastTxId.isNotEmpty ? '$lastTxId - ' : '';
+ _onError?.call(FlutterErrorDetails(
+ exception: '$prefix$e',
+ stack: stacktrace,
+ library: runtimeType.toString(),
+ ));
+ return AddressHistoryResult(
+ transactions: {},
+ missingHistoryItems: const [],
+ hasHistory: false,
+ hadFailures: true,
+ );
+ }
+ }
+
+ // Tracks how many times we've resolved a tx and still couldn't determine
+ // its owning account. Used by `_markUsedAndSplitHistory` to stop
+ // requeueing a tx forever when its account can never be determined.
+ void _trackAccountResolution(ElectrumTransactionInfo tx) {
+ if (type != WalletType.bitcoin) return;
+
+ if (tx.accountIndex == null) {
+ _unresolvedAccountRetryCount.update(tx.id, (count) => count + 1, ifAbsent: () => 1);
+ } else {
+ _unresolvedAccountRetryCount.remove(tx.id);
+ }
+ }
+
+ Future<Map<String, ElectrumTransactionInfo>> _resolveTransactions(
+ List<Map<String, dynamic>> missingHistoryItems, int historyChunkSize) async {
+ final Map<String, ElectrumTransactionInfo> resolved = {};
+
+ if (missingHistoryItems.isEmpty) return resolved;
+
+ if (!shouldUseBatchFetching) {
+ await Future.wait(missingHistoryItems.map((item) async {
+ final txid = (item['tx_hash'] as String).trim();
+ if (txid.isEmpty) return;
+
+ try {
+ final tx = await fetchTransactionInfo(
+ hash: txid,
+ height: item['height'] as int?,
+ retryOnFailure: true,
+ );
+
+ if (tx == null) return;
+
+ resolved[tx.id] = tx;
+
+ _trackAccountResolution(tx);
+
+ _applyLitecoinPegOutTag(tx);
+
+ transactionHistory.addOne(tx);
+ await transactionHistory.save();
+ } catch (e, stacktrace) {
+ _onError?.call(FlutterErrorDetails(
+ exception: '$txid - $e',
+ stack: stacktrace,
+ library: runtimeType.toString(),
+ ));
+ }
+ }));
+
+ return resolved;
+ }
+
+ try {
for (var i = 0; i < missingHistoryItems.length; i += historyChunkSize) {
final end = (i + historyChunkSize < missingHistoryItems.length)
? i + historyChunkSize
@@ -3010,33 +3276,231 @@ abstract class ElectrumWalletBase
retryDelay: const Duration(seconds: 1),
);
+ bool chunkUpdatedHistory = false;
+
for (final txid in hashes) {
final tx = infosByHash[txid];
if (tx == null) continue;
- historiesWithDetails[tx.id] = tx;
+ resolved[tx.id] = tx;
+
+ _trackAccountResolution(tx);
_applyLitecoinPegOutTag(tx);
transactionHistory.addOne(tx);
- didUpdateHistory = true;
+ chunkUpdatedHistory = true;
}
- }
- if (didUpdateHistory) {
- await transactionHistory.save();
+ if (chunkUpdatedHistory) {
+ await transactionHistory.save();
+ }
}
- return historiesWithDetails;
+ return resolved;
} catch (e, stacktrace) {
- final prefix = lastTxId.isNotEmpty ? '$lastTxId - ' : '';
_onError?.call(FlutterErrorDetails(
- exception: '$prefix$e',
+ exception: '$e',
stack: stacktrace,
library: runtimeType.toString(),
));
- return {};
+ return resolved;
+ }
+ }
+
+ Future<void> fetchBitcoinTransactionsForInitialRestore(
+ Map<String, ElectrumTransactionInfo> historiesWithDetails,
+ ) async {
+ int accountIndex = 0;
+
+ while (accountIndex < maxProbAccounts) {
+ final isAccountProbe = accountIndex > 0;
+
+ if (!mainHdByTypeAndAccount.containsKey(accountIndex)) {
+ _prepareHdForAccount(accountIndex, currency);
+ }
+
+ if (!walletAddresses.accountIndexes.contains(accountIndex)) {
+ walletAddresses.accountIndexes.add(accountIndex);
+ await walletAddresses.prepareAccountAddresses(
+ accountIndex,
+ types: isAccountProbe ? EXTRA_ACCOUNT_ADDRESS_TYPES : BITCOIN_ADDRESS_TYPES,
+ );
+ }
+
+ final probeTypes = isAccountProbe ? EXTRA_ACCOUNT_ADDRESS_TYPES : BITCOIN_ADDRESS_TYPES;
+
+ for (final addressType in probeTypes) {
+ if (shouldUseBatchFetching) {
+ await fetchTransactionsForAddressTypeBatch(
+ historiesWithDetails,
+ addressType,
+ accountIndex: accountIndex,
+ );
+ } else {
+ await fetchTransactionsForAddressType(
+ historiesWithDetails,
+ addressType,
+ accountIndex: accountIndex,
+ );
+ }
+ }
+
+ final accountHasHistory = walletAddresses.allAddresses
+ .where((addr) => addr.accountIndex == accountIndex)
+ .any((addr) => addr.isUsed);
+
+ if (!accountHasHistory) {
+ final persistedAccounts = await walletInfo.getAccounts();
+ final isUserAccount =
+ persistedAccounts.any((account) => account.accountIndex == accountIndex);
+
+ if (isAccountProbe && !isUserAccount) {
+ walletAddresses.accountIndexes.remove(accountIndex);
+ walletAddresses.removeAddressesForAccount(accountIndex);
+ }
+ break;
+ }
+
+ // If the account has history, add it to the wallet (if it's not the first account).
+ if (isAccountProbe) {
+ await walletInfo.addAccount(
+ accountIndex: accountIndex,
+ label: "Account $accountIndex",
+ );
+ }
+ // Move on to the next account index
+ accountIndex++;
+ }
+ if (electrumClient.isConnected) {
+ await walletInfo.setAccountDiscoveryLimit(maxProbAccounts);
+ }
+ }
+
+ Future<void> fetchTransactionsForAddressTypeBatch(
+ Map<String, ElectrumTransactionInfo> historiesWithDetails,
+ BitcoinAddressType type, {
+ int? accountIndex,
+ }) async {
+ final addressesByType = walletAddresses.allAddresses
+ .where((addr) => addr.type == type)
+ .where((addr) => accountIndex == null || addr.accountIndex == accountIndex)
+ .toList();
+
+ walletAddresses.hiddenAddresses
+ .addAll(addressesByType.where((addr) => addr.isHidden).map((e) => e.address));
+ await walletAddresses.saveAddressesInBox();
+
+ final accountIndexes = addressesByType.map((addr) => addr.accountIndex).toSet();
+
+ for (final account in accountIndexes) {
+ for (final isHidden in [false, true]) {
+ for (final isLegacyDerivation in [false, true]) {
+ await fetchTransactionsForAddressesBranchBatch(
+ historiesWithDetails,
+ type,
+ getAddressBranchByType(
+ type: type,
+ hidden: isHidden,
+ legacy: isLegacyDerivation,
+ accountIndex: account,
+ ),
+ isHidden: isHidden,
+ isLegacyDerivation: isLegacyDerivation,
+ accountIndex: account,
+ );
+ }
+ }
+ }
+ }
+
+
+ Future<void> fetchTransactionsForAddressesBranchBatch(
+ Map<String, ElectrumTransactionInfo> historiesWithDetails,
+ BitcoinAddressType type,
+ List<BitcoinAddressRecord> branchAddresses, {
+ required bool isHidden,
+ required bool isLegacyDerivation,
+ required int accountIndex,
+ }) async {
+ if (branchAddresses.isEmpty) return;
+
+ final tip = await getCurrentChainTip();
+ final currentBranch = [...branchAddresses];
+
+ final initialHistory =
+ await _processChunksToMap<BitcoinAddressRecord, String, ElectrumTransactionInfo>(
+ items: currentBranch,
+ chunkSize: addressHistoryChunkSize,
+ processChunk: (chunk) async =>
+ (await _fetchAddressHistories(chunk, tip, addressHistoryChunkSize)).transactions,
+ );
+
+ if (initialHistory.isNotEmpty) {
+ historiesWithDetails.addAll(initialHistory);
+ }
+
+ final gapLimit = isHidden
+ ? ElectrumWalletAddressesBase.defaultChangeAddressesCount
+ : ElectrumWalletAddressesBase.defaultReceiveAddressesCount;
+
+ final highestUsedIndex = _highestUsedIndex(currentBranch);
+ final shouldDiscover =
+ highestUsedIndex >= 0 && highestUsedIndex >= currentBranch.length - gapLimit;
+
+ if (!shouldDiscover) return;
+
+ final newAddresses = await walletAddresses.discoverAddressesBatch(
+ currentBranch,
+ isHidden,
+ (newAddresses) async {
+ final newHistory =
+ await _fetchAddressHistories(newAddresses, tip, discoveryHistoryChunkSize);
+
+ if (newHistory.transactions.isNotEmpty) {
+ historiesWithDetails.addAll(newHistory.transactions);
+ }
+
+ return newAddresses
+ .where((addressRecord) => addressRecord.isUsed)
+ .map((addressRecord) => addressRecord.address)
+ .toSet();
+ },
+ type: type,
+ isLegacyDerivation: isLegacyDerivation,
+ accountIndex: accountIndex,
+ );
+
+ if (newAddresses.isNotEmpty) {
+ currentBranch.addAll(newAddresses);
+
+ if (isHidden) {
+ walletAddresses.hiddenAddresses.addAll(newAddresses.map((e) => e.address));
+ await walletAddresses.saveAddressesInBox();
+ }
+ }
+ }
+
+ List<BitcoinAddressRecord> getAddressBranchByType({
+ required BitcoinAddressType type,
+ required bool hidden,
+ required bool legacy,
+ int? accountIndex,
+ }) =>
+ walletAddresses.allAddresses
+ .where((addr) =>
+ addr.type == type &&
+ (accountIndex == null || addr.accountIndex == accountIndex) &&
+ addr.isHidden == hidden &&
+ addr.isLegacyDerivation == legacy)
+ .toList()
+ ..sort((a, b) => a.index.compareTo(b.index));
+
+ int _highestUsedIndex(List<BitcoinAddressRecord> addresses) {
+ for (int i = addresses.length - 1; i >= 0; i--) {
+ if (addresses[i].isUsed) return i;
}
+ return -1;
}
Future<Map<String, Map<String, dynamic>>> _getTransactionVerboseBatch(List<String> hashes) {
@@ -3113,9 +3577,11 @@ abstract class ElectrumWalletBase
required Map<String, ElectrumTransactionInfo?> result,
required Map<String, int?>? heightsByHash,
}) async {
+ final addressesByAccount = addressesSetByAccount;
+
for (var i = 0; i < txIds.length; i += transactionChunkSize) {
final end =
- (i + transactionChunkSize < txIds.length) ? i + transactionChunkSize : txIds.length;
+ (i + transactionChunkSize < txIds.length) ? i + transactionChunkSize : txIds.length;
final chunk = txIds.sublist(i, end);
final bundlesByHash = await getTransactionExpandedBatch(
@@ -3131,14 +3597,17 @@ abstract class ElectrumWalletBase
continue;
}
+ final owner = _accountForTransaction(bundle, addressesByAccount);
+
final info = ElectrumTransactionInfo.fromElectrumBundle(
bundle,
walletInfo.type,
network,
- addresses: addressesSet,
+ addresses: owner != null ? addressesByAccount[owner]! : addressesSet,
height: heightsByHash?[txId],
);
info.id = txId;
+ info.accountIndex = owner;
result[txId] = info;
} catch (_) {
result[txId] = null;
@@ -3399,10 +3868,14 @@ abstract class ElectrumWalletBase
Future<void> updateTransactions() async {
printV("updateTransactions() called!");
+
+ if (_isTransactionUpdating) {
+ printV("updateTransactions: already running, skipping");
+ return;
+ }
+
+ _isTransactionUpdating = true;
try {
- if (_isTransactionUpdating) {
- return;
- }
currentChainTip = await getUpdatedChainTip();
bool updated = false;
@@ -3425,13 +3898,12 @@ abstract class ElectrumWalletBase
await transactionHistory.save();
}
- _isTransactionUpdating = true;
await fetchTransactions();
walletAddresses.updateReceiveAddresses();
- _isTransactionUpdating = false;
} catch (e, stacktrace) {
printV(stacktrace);
printV(e);
+ } finally {
_isTransactionUpdating = false;
}
}
@@ -3464,6 +3936,7 @@ abstract class ElectrumWalletBase
await updateBalance();
await _fetchAddressHistory(address, await getCurrentChainTip());
+ await resolveQueuedTransactionDetails();
} catch (e, s) {
printV("sub error: $e");
_onError?.call(FlutterErrorDetails(
@@ -3530,13 +4003,36 @@ abstract class ElectrumWalletBase
.where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress)
.toList();
- final balances = shouldUseBatchFetching
- ? await fetchBalancesBatch(addresses)
- : await fetchBalancesRegular(addresses);
+ List<Map<String, dynamic>> balances;
+ try {
+ balances = shouldUseBatchFetching
+ ? await fetchBalancesBatch(addresses)
+ : await fetchBalancesRegular(addresses);
+ } catch (e) {
+ printV("fetchBalances network error, returning last known balance: $e");
+ syncStatus = LostConnectionSyncStatus();
+ return balance[currency] ??
+ ElectrumBalance(
+ confirmed: Money.zero(currency),
+ unconfirmed: Money.zero(currency),
+ frozen: Money.zero(currency),
+ );
+ }
printV(
'Fetched balances for ${addresses.length} addresses. Batch fetching: $shouldUseBatchFetching');
+ // If the balances list is empty (no addresses or all fetches silently returned nothing),
+ // preserve the last known balance rather than returning zero.
+ if (balances.isEmpty) {
+ return balance[currency] ??
+ ElectrumBalance(
+ confirmed: Money.zero(currency),
+ unconfirmed: Money.zero(currency),
+ frozen: Money.zero(currency),
+ );
+ }
+
var totalFrozen = 0;
var totalConfirmed = 0;
var totalUnconfirmed = 0;
@@ -3571,7 +4067,9 @@ abstract class ElectrumWalletBase
});
});
- if (balances.isNotEmpty && balances.first['confirmed'] == null) {
+ // Check if ALL returned balances have null confirmed — indicates server is unreachable.
+ final allNull = balances.every((b) => b['confirmed'] == null);
+ if (allNull) {
// if we got null balance responses from the server, set our connection status to lost and return our last known balance:
printV("got null balance responses from the server, setting connection status to lost");
syncStatus = LostConnectionSyncStatus();
@@ -3605,10 +4103,56 @@ abstract class ElectrumWalletBase
);
}
+ List<ElectrumTransactionInfo> get currentAccountBitcoinTransactions {
+ final all = transactionHistory.transactions.values;
+ if (!hasAccountsSupport) return all.toList();
+
+ final accountIndex = currentAccountIndex;
+ final accountAddresses = walletAddresses.allAddresses
+ .where((a) => a.accountIndex == accountIndex)
+ .map((a) => a.address)
+ .toSet();
+
+ return all.where((tx) {
+
+ if (tx.additionalInfo["isLightning"] == true) return accountIndex == 0;
+ if (tx.isReceivedSilentPayment) return accountIndex == 0;
+
+ // Locally created transactions store the account index explicitly,
+ // so use it first instead of checking the addresses.
+ if (tx.accountIndex != null) return tx.accountIndex == accountIndex;
+
+ return (tx.inputAddresses ?? const <String>[]).any(accountAddresses.contains) ||
+ (tx.outputAddresses ?? const <String>[]).any(accountAddresses.contains);
+ }).toList();
+ }
+
Future<void> updateBalance() async {
printV("updateBalance() called!");
- balance[currency] = await fetchBalances();
- await save();
+
+ if (!electrumClient.isConnected) {
+ printV("updateBalance: not connected, keeping existing balance");
+ return;
+ }
+
+ try {
+ final fetchedTotal = await fetchBalances();
+
+ if (type == WalletType.bitcoin && hasAccountsSupport) {
+ // If the wallet has accounts support, we only want to update the balance for the current account.
+ final accountBalance = accountBalances[currentAccountIndex];
+ if (accountBalance == null) {
+ return;
+ }
+ balance[currency] = accountBalance;
+ } else {
+ balance[currency] = fetchedTotal;
+ }
+
+ await save();
+ } catch (e) {
+ printV("updateBalance failed: $e");
+ }
}
@override
@@ -3646,7 +4190,7 @@ abstract class ElectrumWalletBase
}
final hd = addressRecord != null
- ? _hdFor(record: addressRecord).childKey(Bip32KeyIndex(addressRecord.index))
+ ? _hdForAddressRecord(record: addressRecord).childKey(Bip32KeyIndex(addressRecord.index))
: mainHd;
final priv = ECPrivate.fromHex(hd.privateKey.privKey.toHex());
@@ -4061,21 +4605,19 @@ abstract class ElectrumWalletBase
}
}
- Bip32Slip10Secp256k1 _hdFor({required BaseBitcoinAddressRecord record}) {
+ Bip32Slip10Secp256k1 _hdForAddressRecord({required BaseBitcoinAddressRecord record}) {
final addrType = record.type;
if (record.isLegacyDerivation) {
- if (record.isHidden) {
- return walletAddresses.legacySideHd;
- } else {
- return walletAddresses.legacyMainHd;
- }
+ return record.isHidden ? walletAddresses.legacySideHd : walletAddresses.legacyMainHd;
}
+ final accountIndex = record.accountIndex;
+
if (record.isHidden) {
- return sideHdByType[addrType] ?? sideHd;
+ return sideHdByTypeAndAccount[accountIndex]?[addrType] ?? sideHd;
} else {
- return mainHdByType[addrType] ?? mainHd;
+ return mainHdByTypeAndAccount[accountIndex]?[addrType] ?? mainHd;
}
}
}
@@ -4570,3 +5112,27 @@ class UtxoDetails {
required this.spendsUnconfirmedTX,
});
}
+
+class AddressHistoryScan {
+ AddressHistoryScan({
+ required this.historyByScriptHash,
+ required this.hadFailures,
+ });
+
+ final Map<String, List<Map<String, dynamic>>> historyByScriptHash;
+ final bool hadFailures;
+}
+
+class AddressHistoryResult {
+ AddressHistoryResult({
+ required this.transactions,
+ required this.missingHistoryItems,
+ required this.hasHistory,
+ required this.hadFailures,
+ });
+
+ final Map<String, ElectrumTransactionInfo> transactions;
+ final List<Map<String, dynamic>> missingHistoryItems;
+ final bool hasHistory;
+ final bool hadFailures;
+}
\ No newline at end of file
### cw_bitcoin/lib/electrum_wallet_addresses.dart
@@ -1,5 +1,6 @@
import 'dart:io' show Platform;
import 'dart:math';
+import "package:collection/collection.dart";
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
@@ -20,6 +21,15 @@ import 'package:mobx/mobx.dart';
part 'electrum_wallet_addresses.g.dart';
+class UnsupportedAddressTypeForAccountException implements Exception {
+ UnsupportedAddressTypeForAccountException(this.message);
+
+ final String message;
+
+ @override
+ String toString() => message;
+}
+
abstract class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses;
const List<BitcoinAddressType> BITCOIN_ADDRESS_TYPES = [
@@ -30,6 +40,11 @@ const List<BitcoinAddressType> BITCOIN_ADDRESS_TYPES = [
P2shAddressType.p2wpkhInP2sh,
];
+const List<BitcoinAddressType> LEGACY_DUPLICATE_ADDRESS_TYPES = [
+ SegwitAddresType.p2wpkh,
+ SegwitAddresType.p2wsh,
+];
+
const List<BitcoinAddressType> LITECOIN_ADDRESS_TYPES = [
SegwitAddresType.p2wpkh,
SegwitAddresType.mweb,
@@ -43,11 +58,15 @@ const List<BitcoinAddressType> DOGECOIN_ADDRESS_TYPES = [
P2pkhAddressType.p2pkh,
];
+const List<BitcoinAddressType> EXTRA_ACCOUNT_ADDRESS_TYPES = [SegwitAddresType.p2wpkh];
+
abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
ElectrumWalletAddressesBase(
WalletInfo walletInfo, {
- required this.mainHdByType,
- required this.sideHdByType,
+ required this.mainHdByTypeAndAccount,
+ required this.sideHdByTypeAndAccount,
+ required this.accountIndexes,
+ required this.currentAccountIndex,
required this.legacyMainHd,
required this.legacySideHd,
required this.network,
@@ -72,12 +91,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
.toSet()),
currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {},
currentChangeAddressIndexByType = initialChangeAddressIndex ?? {},
- _addressPageType = initialAddressPageType ??
- (walletInfo.addressPageType != null
- ? walletInfo.addressPageType == LightningAddressType.p2l.value
- ? LightningAddressType.p2l
- : BitcoinAddressType.fromValue(walletInfo.addressPageType!)
- : SegwitAddresType.p2wpkh),
+ _addressPageType = _resolveInitialAddressPageType(
+ initialAddressPageType: initialAddressPageType,
+ walletInfo: walletInfo,
+ mainHdByTypeAndAccount: mainHdByTypeAndAccount,
+ currentAccountIndex: currentAccountIndex,
+ ),
silentAddresses = ObservableList<BitcoinSilentPaymentAddressRecord>.of(
(initialSilentAddresses ?? []).toSet()),
currentSilentAddressIndex = initialSilentAddressIndex,
@@ -142,8 +161,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
// TODO: add this variable in `litecoin_wallet_addresses` and just add a cast in cw_bitcoin to use it
final ObservableList<BitcoinAddressRecord> mwebAddresses;
final BasedUtxoNetwork network;
- Map<BitcoinAddressType, Bip32Slip10Secp256k1> mainHdByType;
- Map<BitcoinAddressType, Bip32Slip10Secp256k1> sideHdByType;
+ final Map<int, Map<BitcoinAddressType, Bip32Slip10Secp256k1>> mainHdByTypeAndAccount;
+ final Map<int, Map<BitcoinAddressType, Bip32Slip10Secp256k1>> sideHdByTypeAndAccount;
+ List<int> accountIndexes;
+
+ @observable
+ int currentAccountIndex;
final Bip32Slip10Secp256k1 legacyMainHd;
final Bip32Slip10Secp256k1 legacySideHd;
final bool isHardwareWallet;
@@ -199,8 +222,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
"Error: Unable to fetch your Lightning address, please check your network connection.";
}
- final typeMatchingAddressesAll =
- _addresses.where((addr) => !addr.isHidden && _isAddressPageTypeMatch(addr)).toList();
+ final accountIndexForCheck = walletInfo.type == WalletType.bitcoin ? currentAccountIndex : 0;
+ if (!_isAddressTypeSupportedForAccount(addressPageType, accountIndexForCheck)) {
+ printV("addressPageType $addressPageType is not supported for account $accountIndexForCheck");
+ return "";
+ }
+
+ final typeMatchingAddressesAll = _addresses
+ .where((addr) =>
+ _isCurrentAccountAddress(addr) && !addr.isHidden && _isAddressPageTypeMatch(addr))
+ .toList();
// Prefer standard derivation addresses for the current/active address,
// but keep legacy addresses present in the overall address lists.
@@ -238,6 +269,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
final prev = previousAddressRecord;
if (prev != null &&
+ _isCurrentAccountAddress(prev) &&
prev.type == addressPageType &&
!prev.isUsed &&
!prev.isLegacyDerivation &&
@@ -303,8 +335,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return silentAddress?.toString() ?? '';
}
- final mainHd = mainHdByType[addressPageType] ?? mainHdByType.values.first;
- return getAddress(index: 0, hd: mainHd, addressType: addressPageType);
+ return _firstAddressForType(addressPageType);
}
String get payjoinCompatibleAddress {
@@ -313,7 +344,18 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
? SegwitAddresType.p2wpkh
: addressPageType;
- final mainHd = mainHdByType[addrType] ?? mainHdByType.values.first;
+ return _firstAddressForType(addrType);
+ }
+
+ String _firstAddressForType(BitcoinAddressType addrType) {
+ final mainMap = walletInfo.type == WalletType.bitcoin
+ ? mainHdByTypeAndAccount[currentAccountIndex]
+ : mainHdByTypeAndAccount[0];
+
+ final mainHd = mainMap?[addrType] ?? mainMap?.values.first;
+
+ if (mainHd == null) return '';
+
return getAddress(index: 0, hd: mainHd, addressType: addrType);
}
@@ -355,7 +397,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
});
@override
- Future<void> init() async {
+ Future<void> init({List<int> accountIndexes = const []}) async {
+ final effectiveAccountIndexes =
+ accountIndexes.isNotEmpty ? accountIndexes : this.accountIndexes;
+
+ if (accountIndexes.isNotEmpty) this.accountIndexes = accountIndexes;
if (walletInfo.type == WalletType.bitcoinCash) {
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
} else if (walletInfo.type == WalletType.litecoin) {
@@ -366,21 +412,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
} else if (walletInfo.type == WalletType.dogecoin) {
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
} else if (walletInfo.type == WalletType.bitcoin) {
- await _generateInitialAddresses(isLegacyDerivation: true);
- await _generateInitialAddresses();
- if (!isHardwareWallet) {
- await _generateInitialAddresses(type: P2pkhAddressType.p2pkh, isLegacyDerivation: true);
- await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
-
- await _generateInitialAddresses(
- type: P2shAddressType.p2wpkhInP2sh, isLegacyDerivation: true);
- await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh);
+ for (final accountIndex in effectiveAccountIndexes) {
- await _generateInitialAddresses(type: SegwitAddresType.p2tr, isLegacyDerivation: true);
- await _generateInitialAddresses(type: SegwitAddresType.p2tr);
-
- await _generateInitialAddresses(type: SegwitAddresType.p2wsh, isLegacyDerivation: true);
- await _generateInitialAddresses(type: SegwitAddresType.p2wsh);
+ await prepareAccountAddresses(
+ accountIndex,
+ types: accountIndex == 0 ? BITCOIN_ADDRESS_TYPES : EXTRA_ACCOUNT_ADDRESS_TYPES,
+ includeLegacy: accountIndex == 0,
+ );
}
}
@@ -407,9 +445,21 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
updateChangeAddresses();
if (changeAddresses.isEmpty) {
- final newAddresses = await _createNewAddresses(gap,
- startIndex: totalCountOfChangeAddresses > 0 ? totalCountOfChangeAddresses - 1 : 0,
- isHidden: true);
+ final accountChangeCount = _addresses
+ .where((addressRecord) =>
+ _isCurrentAccountAddress(addressRecord) &&
+ addressRecord.isHidden &&
+ addressRecord.type == addressPageType)
+ .length;
+
+ final newAddresses = await _createNewAddresses(
+ gap,
+ startIndex: accountChangeCount,
+ isHidden: true,
+ type: addressPageType,
+ accountIndex: currentAccountIndex,
+ );
+
addAddresses(newAddresses);
}
@@ -423,6 +473,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return address;
}
+ bool _isCurrentAccountAddress(BitcoinAddressRecord addressRecord) {
+ return walletInfo.type != WalletType.bitcoin ||
+ addressRecord.accountIndex == currentAccountIndex;
+ }
+
Map<String, String> get labels {
final G = ECPublic.fromBytes(BigintUtils.toBytes(Curves.generatorSecp256k1.x, length: 32));
final labels = <String, String>{};
@@ -440,6 +495,29 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return labels;
}
+ Future<void> prepareAccountAddresses(
+ int accountIndex, {
+ List<BitcoinAddressType> types = BITCOIN_ADDRESS_TYPES,
+ bool includeLegacy = false,
+ }) async {
+ for (final type in types) {
+ final shouldSkipHardwareWalletType = isHardwareWallet && type != SegwitAddresType.p2wpkh;
+
+ if (shouldSkipHardwareWalletType) continue;
+
+ await _generateInitialAddresses(accountIndex: accountIndex, type: type);
+
+ // Legacy derivation for these types is identical to the standard one.
+ if (includeLegacy && !LEGACY_DUPLICATE_ADDRESS_TYPES.contains(type)) {
+ await _generateInitialAddresses(
+ accountIndex: accountIndex,
+ type: type,
+ isLegacyDerivation: true,
+ );
+ }
+ }
+ }
+
@action
BaseBitcoinAddressRecord generateNewAddress({String label = ''}) {
if (addressPageType is LightningAddressType) {
@@ -471,13 +549,26 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return address;
}
- final newAddressIndex = addressesByReceiveType.fold(
- 0, (int acc, addressRecord) => addressRecord.isHidden == false ? acc + 1 : acc);
+ final accountIndex = walletInfo.type == WalletType.bitcoin ? currentAccountIndex : 0;
- final hd = _hdFor(isHidden: false, type: addressPageType, isLegacyDerivation: false);
+ final newAddressIndex = addressesByReceiveType.fold(
+ 0,
+ (int acc, addressRecord) => addressRecord is BitcoinAddressRecord &&
+ addressRecord.accountIndex == accountIndex &&
+ addressRecord.isHidden == false
+ ? acc + 1
+ : acc);
+
+ final hd = _hdForAddressGeneration(
+ isHidden: false,
+ type: addressPageType,
+ isLegacyDerivation: false,
+ accountIndex: accountIndex,
+ );
final address = BitcoinAddressRecord(
getAddress(index: newAddressIndex, hd: hd, addressType: addressPageType),
index: newAddressIndex,
+ accountIndex: accountIndex,
isHidden: false,
isLegacyDerivation: false,
name: label,
@@ -508,45 +599,54 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
void addBitcoinAddressTypes() {
final lastP2wpkh = _addresses
.where((addressRecord) =>
- _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh))
- .toList()
- .last;
- if (lastP2wpkh.address != address) {
- addressesMap[lastP2wpkh.address] = 'P2WPKH';
- } else {
- addressesMap[address] = 'Active - P2WPKH';
+ _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh))
+ .lastOrNull;
+ if (lastP2wpkh != null) {
+ if (lastP2wpkh.address != address) {
+ addressesMap[lastP2wpkh.address] = 'P2WPKH';
+ } else {
+ addressesMap[address] = 'Active - P2WPKH';
+ }
}
- final lastP2pkh = _addresses.firstWhere(
- (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh));
- if (lastP2pkh.address != address) {
- addressesMap[lastP2pkh.address] = 'P2PKH';
- } else {
- addressesMap[address] = 'Active - P2PKH';
+ final lastP2pkh = _addresses.firstWhereOrNull(
+ (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh));
+ if (lastP2pkh != null) {
+ if (lastP2pkh.address != address) {
+ addressesMap[lastP2pkh.address] = 'P2PKH';
+ } else {
+ addressesMap[address] = 'Active - P2PKH';
+ }
}
- final lastP2sh = _addresses.firstWhere((addressRecord) =>
+ final lastP2sh = _addresses.firstWhereOrNull((addressRecord) =>
_isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh));
- if (lastP2sh.address != address) {
- addressesMap[lastP2sh.address] = 'P2SH';
- } else {
- addressesMap[address] = 'Active - P2SH';
+ if (lastP2sh != null) {
+ if (lastP2sh.address != address) {
+ addressesMap[lastP2sh.address] = 'P2SH';
+ } else {
+ addressesMap[address] = 'Active - P2SH';
+ }
}
- final lastP2tr = _addresses.firstWhere(
- (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr));
- if (lastP2tr.address != address) {
- addressesMap[lastP2tr.address] = 'P2TR';
- } else {
- addressesMap[address] = 'Active - P2TR';
+ final lastP2tr = _addresses.firstWhereOrNull(
+ (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr));
+ if (lastP2tr != null) {
+ if (lastP2tr.address != address) {
+ addressesMap[lastP2tr.address] = 'P2TR';
+ } else {
+ addressesMap[address] = 'Active - P2TR';
+ }
}
- final lastP2wsh = _addresses.firstWhere(
- (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh));
- if (lastP2wsh.address != address) {
- addressesMap[lastP2wsh.address] = 'P2WSH';
- } else {
- addressesMap[address] = 'Active - P2WSH';
+ final lastP2wsh = _addresses.firstWhereOrNull(
+ (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh));
+ if (lastP2wsh != null) {
+ if (lastP2wsh.address != address) {
+ addressesMap[lastP2wsh.address] = 'P2WSH';
+ } else {
+ addressesMap[address] = 'Active - P2WSH';
+ }
}
final firstSilentAddressRecord = silentAddresses.firstOrNull;
@@ -678,22 +778,28 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
addressesByReceiveType.clear();
addressesByReceiveType.addAll(
- _addresses.where(_isAddressPageTypeMatch).toList(),
+ _addresses
+ .where((addressRecord) =>
+ _isCurrentAccountAddress(addressRecord) && _isAddressPageTypeMatch(addressRecord))
+ .toList(),
);
}
@action
void updateReceiveAddresses() {
receiveAddresses.removeRange(0, receiveAddresses.length);
- final newAddresses =
- _addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed);
+ final newAddresses = _addresses.where((addressRecord) =>
+ _isCurrentAccountAddress(addressRecord) &&
+ !addressRecord.isHidden &&
+ !addressRecord.isUsed);
receiveAddresses.addAll(newAddresses);
}
@action
void updateChangeAddresses() {
changeAddresses.removeRange(0, changeAddresses.length);
final newAddresses = _addresses.where((addressRecord) =>
+ _isCurrentAccountAddress(addressRecord) &&
addressRecord.isHidden &&
!addressRecord.isUsed &&
// TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type
@@ -708,13 +814,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
Future<String?> Function(BitcoinAddressRecord) getAddressHistory, {
BitcoinAddressType type = SegwitAddresType.p2wpkh,
required bool isLegacyDerivation,
+ int accountIndex = 0,
}) async {
final newAddresses = await _createNewAddresses(
gap,
startIndex: addressList.length,
isHidden: isHidden,
isLegacyDerivation: isLegacyDerivation,
type: type,
+ accountIndex: accountIndex,
);
addAddresses(newAddresses);
@@ -730,6 +838,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
getAddressHistory,
type: type,
isLegacyDerivation: isLegacyDerivation,
+ accountIndex: accountIndex,
);
}
}
@@ -741,13 +850,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
Future<Set<String>> Function(List<BitcoinAddressRecord>) getUsedAddresses, {
BitcoinAddressType type = SegwitAddresType.p2wpkh,
required bool isLegacyDerivation,
+ int accountIndex = 0,
}) async {
final newAddresses = await _createNewAddresses(
gap,
startIndex: addressList.length,
isHidden: isHidden,
type: type,
isLegacyDerivation: isLegacyDerivation,
+ accountIndex: accountIndex,
);
addAddresses(newAddresses);
@@ -768,24 +879,26 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
getUsedAddresses,
type: type,
isLegacyDerivation: isLegacyDerivation,
+ accountIndex: accountIndex,
);
return [...newAddresses, ...moreNewAddresses];
}
- Future<void> _generateInitialAddresses(
- {BitcoinAddressType type = SegwitAddresType.p2wpkh, bool isLegacyDerivation = false}) async {
- // Legacy derivation produces the same addresses as standard for these types.
- // Don't generate a legacy set to avoid duplicates.
- if (isLegacyDerivation && (type == SegwitAddresType.p2wpkh || type == SegwitAddresType.p2wsh)) {
- return;
- }
+ Future<void> _generateInitialAddresses({
+ BitcoinAddressType type = SegwitAddresType.p2wpkh,
+ bool isLegacyDerivation = false,
+ int accountIndex = 0,
+ }) async {
+ if (isLegacyDerivation && accountIndex != 0) return;
var countOfReceiveAddresses = 0;
var countOfHiddenAddresses = 0;
_addresses.forEach((addr) {
- if (addr.type == type && addr.isLegacyDerivation == isLegacyDerivation) {
+ if (addr.accountIndex == accountIndex &&
+ addr.type == type &&
+ addr.isLegacyDerivation == isLegacyDerivation) {
if (addr.isHidden) {
countOfHiddenAddresses += 1;
} else {
@@ -800,7 +913,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
startIndex: countOfReceiveAddresses,
isHidden: false,
type: type,
- isLegacyDerivation: isLegacyDerivation);
+ isLegacyDerivation: isLegacyDerivation,
+ accountIndex: accountIndex);
+
addAddresses(newAddresses);
}
@@ -810,31 +925,48 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
startIndex: countOfHiddenAddresses,
isHidden: true,
type: type,
- isLegacyDerivation: isLegacyDerivation);
+ isLegacyDerivation: isLegacyDerivation,
+ accountIndex: accountIndex);
addAddresses(newAddresses);
}
}
- Future<List<BitcoinAddressRecord>> _createNewAddresses(int count,
- {int startIndex = 0,
- bool isHidden = false,
- BitcoinAddressType? type,
- bool isLegacyDerivation = false}) async {
+ Future<List<BitcoinAddressRecord>> _createNewAddresses(
+ int count, {
+ int startIndex = 0,
+ bool isHidden = false,
+ BitcoinAddressType? type,
+ bool isLegacyDerivation = false,
+ int accountIndex = 0,
+ }) async {
final list = <BitcoinAddressRecord>[];
for (var i = startIndex; i < count + startIndex; i++) {
final addrType = type ?? addressPageType;
- final hd = _hdFor(isHidden: isHidden, type: addrType, isLegacyDerivation: isLegacyDerivation);
- final address = BitcoinAddressRecord(
- await getAddressAsync(index: i, hd: hd, addressType: addrType),
- index: i,
- isHidden: isHidden,
- isLegacyDerivation: isLegacyDerivation,
- type: addrType,
- network: network,
- );
- list.add(address);
+ try {
+ final hd = _hdForAddressGeneration(
+ isHidden: isHidden,
+ type: addrType,
+ isLegacyDerivation: isLegacyDerivation,
+ accountIndex: accountIndex,
+ );
+
+ final address = BitcoinAddressRecord(
+ await getAddressAsync(index: i, hd: hd, addressType: addrType),
+ index: i,
+ isHidden: isHidden,
+ isLegacyDerivation: isLegacyDerivation,
+ type: addrType,
+ network: network,
+ accountIndex: accountIndex,
+ );
+ list.add(address);
+ } on UnsupportedAddressTypeForAccountException catch (e) {
+ printV("_createNewAddresses: skipping index $i, type $addrType, "
+ "account $accountIndex: $e");
+ return [];
+ }
}
return list;
@@ -876,18 +1008,30 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return;
}
- final mainHd = _hdFor(
- isHidden: false, type: element.type, isLegacyDerivation: element.isLegacyDerivation);
- final sideHd = _hdFor(
- isHidden: true, type: element.type, isLegacyDerivation: element.isLegacyDerivation);
- if (!element.isHidden &&
- element.address !=
- await getAddressAsync(index: element.index, hd: mainHd, addressType: element.type)) {
- element.isHidden = true;
- } else if (element.isHidden &&
- element.address !=
- await getAddressAsync(index: element.index, hd: sideHd, addressType: element.type)) {
- element.isHidden = false;
+ try {
+ final mainHd = _hdForAddressGeneration(
+ isHidden: false,
+ type: element.type,
+ isLegacyDerivation: element.isLegacyDerivation,
+ accountIndex: element.accountIndex);
+ final sideHd = _hdForAddressGeneration(
+ isHidden: true,
+ type: element.type,
+ isLegacyDerivation: element.isLegacyDerivation,
+ accountIndex: element.accountIndex);
+ if (!element.isHidden &&
+ element.address !=
+ await getAddressAsync(
+ index: element.index, hd: mainHd, addressType: element.type)) {
+ element.isHidden = true;
+ } else if (element.isHidden &&
+ element.address !=
+ await getAddressAsync(
+ index: element.index, hd: sideHd, addressType: element.type)) {
+ element.isHidden = false;
+ }
+ } on UnsupportedAddressTypeForAccountException catch (e) {
+ printV("_validateAddresses: skipping ${element.address}: $e");
}
});
}
@@ -897,7 +1041,19 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action
Future<void> setAddressType(BitcoinAddressType type) async {
- _addressPageType = type;
+
+ final needsAccountScopedHd =
+ type is! LightningAddressType && type != SilentPaymentsAddresType.p2sp;
+
+ BitcoinAddressType resolvedType = type;
+ if (needsAccountScopedHd) {
+ final accountIndex = walletInfo.type == WalletType.bitcoin ? currentAccountIndex : 0;
+ if (!_isAddressTypeSupportedForAccount(type, accountIndex)) {
+ resolvedType = EXTRA_ACCOUNT_ADDRESS_TYPES.first;
+ }
+ }
+
+ _addressPageType = resolvedType;
updateAddressesByMatch();
walletInfo.addressPageType = addressPageType.toString();
await walletInfo.save();
@@ -909,8 +1065,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type;
- bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) =>
- !addr.isHidden && !addr.isUsed && addr.type == type;
+ bool _isUnusedReceiveAddressByType(
+ BitcoinAddressRecord addressRecord,
+ BitcoinAddressType type,
+ ) {
+ return _isCurrentAccountAddress(addressRecord) &&
+ !addressRecord.isHidden &&
+ !addressRecord.isUsed &&
+ addressRecord.type == type;
+ }
@action
void deleteSilentPaymentAddress(String address) {
@@ -921,16 +1084,84 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
updateAddressesByMatch();
}
- Bip32Slip10Secp256k1 _hdFor({
+ // Remove all addresses associated with a specific account index.
+ @action
+ Future<void> removeAddressesForAccount(int accountIndex) async {
+ _addresses.removeWhere((addr) => addr.accountIndex == accountIndex);
+ updateAddressesByMatch();
+ updateReceiveAddresses();
+ updateChangeAddresses();
+ await updateAddressesInBox();
+ }
+
+
+ bool _isAddressTypeSupportedForAccount(BitcoinAddressType type, int accountIndex) {
+ final effectiveAccountIndex = walletInfo.type == WalletType.bitcoin ? accountIndex : 0;
+ return mainHdByTypeAndAccount[effectiveAccountIndex]?.containsKey(type) ?? false;
+ }
+
+ static BitcoinAddressType _resolveInitialAddressPageType({
+ required BitcoinAddressType? initialAddressPageType,
+ required WalletInfo walletInfo,
+ required Map<int, Map<BitcoinAddressType, Bip32Slip10Secp256k1>> mainHdByTypeAndAccount,
+ required int currentAccountIndex,
+ }) {
+ final resolved = initialAddressPageType ??
+ (walletInfo.addressPageType != null
+ ? walletInfo.addressPageType == LightningAddressType.p2l.value
+ ? LightningAddressType.p2l
+ : BitcoinAddressType.fromValue(walletInfo.addressPageType!)
+ : SegwitAddresType.p2wpkh);
+
+ // Non-account-scoped address types (Lightning, Silent Payments) don't need
+ // an HD map entry.
+ if (resolved is LightningAddressType || resolved == SilentPaymentsAddresType.p2sp) {
+ return resolved;
+ }
+
+ final effectiveAccountIndex = walletInfo.type == WalletType.bitcoin ? currentAccountIndex : 0;
+
+ final isValid = mainHdByTypeAndAccount[effectiveAccountIndex]?.containsKey(resolved) ?? false;
+
+ if (!isValid) {
+ return EXTRA_ACCOUNT_ADDRESS_TYPES.first;
+ }
+
+ return resolved;
+ }
+
+ Bip32Slip10Secp256k1 _hdForAddressGeneration({
required bool isHidden,
required BitcoinAddressType type,
required bool isLegacyDerivation,
+ int accountIndex = 0,
}) {
- if (isLegacyDerivation) return isHidden ? legacySideHd : legacyMainHd;
+ if (isLegacyDerivation) {
+ if (accountIndex != 0) {
+ throw UnsupportedAddressTypeForAccountException(
+ "Legacy derivation is only supported for the first account");
+ }
+
+ return isHidden ? legacySideHd : legacyMainHd;
+ }
+
+ final effectiveAccountIndex = walletInfo.type == WalletType.bitcoin ? accountIndex : 0;
+
+ final map = isHidden
+ ? sideHdByTypeAndAccount[effectiveAccountIndex]
+ : mainHdByTypeAndAccount[effectiveAccountIndex];
+
+ if (map == null) {
+ throw UnsupportedAddressTypeForAccountException(
+ "HD map not found for account $effectiveAccountIndex");
+ }
- final map = isHidden ? sideHdByType : mainHdByType;
final hd = map[type];
- if (hd == null) throw Exception("HD not found for type $type");
+ if (hd == null) {
+ throw UnsupportedAddressTypeForAccountException(
+ "HD not found for account $accountIndex type $type");
+ }
+
return hd;
}
### cw_bitcoin/lib/litecoin_wallet.dart
@@ -115,8 +115,10 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
initialMwebAddresses: initialMwebAddresses,
- mainHdByType: mainHdByType,
- sideHdByType: sideHdByType,
+ mainHdByTypeAndAccount: mainHdByTypeAndAccount,
+ sideHdByTypeAndAccount: sideHdByTypeAndAccount,
+ accountIndexes: [currentAccountIndex],
+ currentAccountIndex: currentAccountIndex,
legacyMainHd: mainHd,
legacySideHd: sideHd,
network: network,
@@ -1365,6 +1367,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
final tx = PendingBitcoinTransaction(
btcTx,
type,
+ accountIndex: 0,
electrumClient: electrumClient,
amount: Money.zero(currency),
fee: Money.fromInt(resp.fee.toInt(), currency),
### cw_bitcoin/lib/litecoin_wallet_addresses.dart
@@ -25,8 +25,10 @@ class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalle
abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
LitecoinWalletAddressesBase(
WalletInfo walletInfo, {
- required super.mainHdByType,
- required super.sideHdByType,
+ required super.mainHdByTypeAndAccount,
+ required super.sideHdByTypeAndAccount,
+ required super.accountIndexes,
+ required super.currentAccountIndex,
required super.legacyMainHd,
required super.legacySideHd,
required super.network,
@@ -66,9 +68,9 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
List.filled(32, 0);
@override
- Future<void> init() async {
+ Future<void> init({List<int> accountIndexes = const []}) async {
if (!isHardwareWallet) await initMwebAddresses();
- await super.init();
+ await super.init(accountIndexes: accountIndexes);
}
@computed
### cw_bitcoin/lib/pending_bitcoin_transaction.dart
@@ -21,6 +21,7 @@ class PendingBitcoinTransaction with PendingTransaction {
PendingBitcoinTransaction(
this._tx,
this.type, {
+ required this.accountIndex,
required this.electrumClient,
required this.amount,
required this.fee,
@@ -40,6 +41,7 @@ class PendingBitcoinTransaction with PendingTransaction {
final WalletType type;
final BtcTransaction _tx;
+ final int? accountIndex;
final ElectrumClient electrumClient;
final String feeRate;
final BasedUtxoNetwork? network;
@@ -182,6 +184,7 @@ class PendingBitcoinTransaction with PendingTransaction {
isPending: true,
isReplaced: false,
confirmations: 0,
+ accountIndex: accountIndex,
inputAddresses: _tx.inputs.map((input) => input.txId).toList(),
outputAddresses: outputAddresses,
fee: fee,
### cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart
@@ -56,8 +56,10 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
initialAddresses: initialAddresses,
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
- mainHdByType: mainHdByType,
- sideHdByType: sideHdByType,
+ mainHdByTypeAndAccount: mainHdByTypeAndAccount,
+ sideHdByTypeAndAccount: sideHdByTypeAndAccount,
+ accountIndexes: [currentAccountIndex],
+ currentAccountIndex: currentAccountIndex,
legacyMainHd: mainHd,
legacySideHd: sideHd,
network: network,
### cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart
@@ -13,8 +13,10 @@ class BitcoinCashWalletAddresses = BitcoinCashWalletAddressesBase with _$Bitcoin
abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses with Store {
BitcoinCashWalletAddressesBase(
WalletInfo walletInfo, {
- required super.mainHdByType,
- required super.sideHdByType,
+ required super.mainHdByTypeAndAccount,
+ required super.sideHdByTypeAndAccount,
+ required super.accountIndexes,
+ required super.currentAccountIndex,
required super.legacyMainHd,
required super.legacySideHd,
required super.network,
### cw_core/lib/account.dart
@@ -1,12 +1,32 @@
class Account {
- Account({required this.id, required this.label, this.balance});
+ Account({
+ required this.id,
+ required this.label,
+ String? balance,
+ }) : balance = balance ?? "0.00";
- Account.fromMap(Map<String, Object> map)
- : this.id = map['id'] == null ? 0 : int.parse(map['id'] as String),
- this.label = (map['label'] ?? '') as String,
- this.balance = (map['balance'] ?? '0.00') as String;
+ Account.fromMap(Map<String, Object?> map)
+ : id = map["id"] is int ? map["id"] as int : int.tryParse(map["id"]?.toString() ?? "") ?? 0,
+ label = (map["label"] ?? "") as String,
+ balance = (map["balance"] ?? "0.00") as String;
final int id;
final String label;
- final String? balance;
+ final String balance;
+
+ Account copyWith({
+ int? id,
+ String? label,
+ String? balance,
+ }) => Account(
+ id: id ?? this.id,
+ label: label ?? this.label,
+ balance: balance ?? this.balance,
+ );
+
+ Map<String, Object> toMap() => {
+ "id": id.toString(),
+ "label": label,
+ "balance": balance,
+ };
}
### cw_core/lib/account_list.dart
@@ -1,15 +0,0 @@
-import 'package:mobx/mobx.dart';
-
-abstract class AccountList<T> {
- ObservableList<T> get accounts;
-
- void update();
-
- List<T> getAll();
-
- Future<void> addAccount({required String label});
-
- Future<void> setLabelAccount({required int accountIndex, required String label});
-
- void refresh();
-}
### cw_core/lib/balance_card_style_settings.dart
@@ -98,6 +98,6 @@ class BalanceCardStyleSettings {
}
Future<void> insert() async {
- db!.insert(tableName, toJson(), conflictAlgorithm: ConflictAlgorithm.replace);
+ await db!.insert(tableName, toJson(), conflictAlgorithm: ConflictAlgorithm.replace);
}
}
### cw_core/lib/db/sqlite.dart
@@ -3,18 +3,19 @@ import "dart:io";
import "package:cw_core/db/sqlite_debug.dart";
import "package:cw_core/root_dir.dart";
import "package:cw_core/utils/print_verbose.dart";
+import "package:cw_core/wallet_type.dart";
import "package:flutter/foundation.dart";
import "package:path/path.dart" as p;
import "package:sqflite_common_ffi/sqflite_ffi.dart";
Database? db;
Future<void> _addColumnIfNotExists(
- Database db, {
- required String table,
- required String column,
- required String definition,
-}) async {
+ Database db, {
+ required String table,
+ required String column,
+ required String definition,
+ }) async {
final result = await db.rawQuery("PRAGMA table_info($table)");
final columnExists = result.any((row) => row["name"] == column);
@@ -65,7 +66,7 @@ Future<void> _initDb({String? pathOverride}) async {
await db?.close();
db = await openDatabase(
dbFile.path,
- version: 13,
+ version: 14,
onUpgrade: (db, oldVersion, newVersion) async {
printV("migrating: $oldVersion, $newVersion");
if (oldVersion <= 1) {
@@ -100,77 +101,101 @@ CREATE TABLE IF NOT EXISTS BalanceCardStyleSettings (
definition: "BOOLEAN DEFAULT FALSE",
);
- await _addColumnIfNotExists(
- db,
- table: 'BalanceCardStyleSettings',
- column: 'cardOrder',
- definition: 'INTEGER DEFAULT 0',
- );
- }
- if (oldVersion <= 3) {
- await _addColumnIfNotExists(db,
- table: "WalletInfo", column: "showCombinedBalance", definition: "BOOLEAN DEFAULT TRUE");
- // null - primary token (eth, sol etc)
- // not null - address of fav token
- // if address doesn't correspond to a valid token, fallback to primary token
- await _addColumnIfNotExists(db,
- table: "WalletInfo", column: "favoriteTokenAddress", definition: "TEXT DEFAULT NULL");
- }
+ await _addColumnIfNotExists(
+ db,
+ table: 'BalanceCardStyleSettings',
+ column: 'cardOrder',
+ definition: 'INTEGER DEFAULT 0',
+ );
+ }
+ if (oldVersion <= 3) {
+ await _addColumnIfNotExists(db,
+ table: "WalletInfo", column: "showCombinedBalance", definition: "BOOLEAN DEFAULT TRUE");
+ // null - primary token (eth, sol etc)
+ // not null - address of fav token
+ // if address doesn't correspond to a valid token, fallback to primary token
+ await _addColumnIfNotExists(db,
+ table: "WalletInfo", column: "favoriteTokenAddress", definition: "TEXT DEFAULT NULL");
+ }
- if (oldVersion <= 4) {
- await _createBridgeTransferTable(db);
- }
+ if (oldVersion <= 4) {
+ await _createBridgeTransferTable(db);
+ }
- if (oldVersion <= 5) {
- await _createTradeTable(db);
- }
- if (oldVersion <= 6) {
- await _addColumnIfNotExists(
- db,
- table: 'Trade',
- column: 'toAddressExtraId',
- definition: 'TEXT',
- );
- }
- if (oldVersion <= 7) {
- await _createNodeTable(db);
- }
- if (oldVersion <= 8) {
- await _addColumnIfNotExists(
- db,
- table: 'BalanceCardStyleSettings',
- column: 'iconStyleIndex',
- definition: 'INTEGER DEFAULT 0',
- );
- await _addColumnIfNotExists(
- db,
- table: 'BalanceCardStyleSettings',
- column: 'isGradientOnly',
- definition: 'BOOLEAN DEFAULT FALSE',
- );
- }
- if (oldVersion <= 9) {
- await _createErc20TokenTable(db);
- await _createSplTokenTable(db);
- await _createTronTokenTable(db);
- }
+ if (oldVersion <= 5) {
+ await _createTradeTable(db);
+ }
+ if (oldVersion <= 6) {
+ await _addColumnIfNotExists(
+ db,
+ table: 'Trade',
+ column: 'toAddressExtraId',
+ definition: 'TEXT',
+ );
+ }
+ if (oldVersion <= 7) {
+ await _createNodeTable(db);
+ }
+ if (oldVersion <= 8) {
+ await _addColumnIfNotExists(
+ db,
+ table: 'BalanceCardStyleSettings',
+ column: 'iconStyleIndex',
+ definition: 'INTEGER DEFAULT 0',
+ );
+ await _addColumnIfNotExists(
+ db,
+ table: 'BalanceCardStyleSettings',
+ column: 'isGradientOnly',
+ definition: 'BOOLEAN DEFAULT FALSE',
+ );
+ }
+ if (oldVersion <= 9) {
+ await _createErc20TokenTable(db);
+ await _createSplTokenTable(db);
+ await _createTronTokenTable(db);
+ }
+ if (oldVersion <= 10) {
+ await _createImportedNFTTable(db);
+ }
+ if (oldVersion <= 11) {
+ await _addColumnIfNotExists(
+ db,
+ table: "WalletInfo",
+ column: "showSeedBackupReminder",
+ definition: "BOOLEAN DEFAULT FALSE",
+ );
+ }
+ if (oldVersion <= 12) {
+ await _createChartsTables(db);
+ }
- if (oldVersion <= 10) {
- await _createImportedNFTTable(db);
- }
- if (oldVersion <= 11) {
- await _addColumnIfNotExists(
- db,
- table: "WalletInfo",
- column: "showSeedBackupReminder",
- definition: "BOOLEAN DEFAULT FALSE",
- );
- }
- if(oldVersion <= 12) {
- await _createChartsTables(db);
- }
- }, onCreate: (Database db, int version) async {
- await db.execute('''
+ if (oldVersion <= 13) {
+ await _createWalletInfoAccountTable(db);
+ await _addColumnIfNotExists(
+ db,
+ table: "WalletInfo",
+ column: "accountDiscoveryLimit",
+ definition: "INTEGER DEFAULT NULL",
+ );
+ await _addColumnIfNotExists(
+ db,
+ table: "WalletInfo",
+ column: "isMultiAccountsEnabled",
+ definition: "INTEGER DEFAULT NULL",
+ );
+ await _addColumnIfNotExists(
+ db,
+ table: "WalletInfo",
+ column: "currentAccountIndex",
+ definition: "INTEGER NOT NULL DEFAULT 0",
+ );
+
+ await _migrateBitcoinCardStylesForAccounts(db);
+ }
+ },
+ onCreate: (Database db, int version) async {
+ await db.execute('''
CREATE TABLE WalletInfo (
walletInfoId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
id TEXT NOT NULL,
@@ -193,9 +218,12 @@ CREATE TABLE WalletInfo (
hashedWalletIdentifier TEXT,
isNonSeedWallet INTEGER DEFAULT (0) NOT NULL,
sortOrder INTEGER DEFAULT (0) NOT NULL,
+ currentAccountIndex INTEGER NOT NULL DEFAULT 0,
receiveInfoboxDismissed BOOLEAN DEFAULT FALSE,
showCombinedBalance BOOLEAN DEFAULT TRUE,
favoriteTokenAddress TEXT DEFAULT NULL,
+ accountDiscoveryLimit INTEGER DEFAULT NULL,
+ isMultiAccountsEnabled INTEGER DEFAULT NULL,
showSeedBackupReminder BOOLEAN DEFAULT FALSE
);
''');
@@ -269,8 +297,8 @@ CREATE TABLE BalanceCardStyleSettings (
await _createErc20TokenTable(db);
await _createSplTokenTable(db);
await _createTronTokenTable(db);
- await _createImportedNFTTable(db);
-
+ await _createImportedNFTTable(db);
+ await _createWalletInfoAccountTable(db);
},
);
}
@@ -356,6 +384,43 @@ ON Trade (id);
""");
}
+Future<void> _createWalletInfoAccountTable(Database db) async {
+ await db.execute("""
+CREATE TABLE IF NOT EXISTS WalletInfoAccount (
+ walletInfoId INTEGER NOT NULL,
+ accountIndex INTEGER NOT NULL,
+ label TEXT NOT NULL,
+ PRIMARY KEY (walletInfoId, accountIndex),
+ CONSTRAINT WalletInfoAccount_WalletInfo_FK FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId)
+);
+""");
+
+ await db.execute("""
+CREATE INDEX IF NOT EXISTS idx_walletinfoaccount_walletinfoid
+ON WalletInfoAccount(walletInfoId);
+""");
+}
+
+
+Future<void> _migrateBitcoinCardStylesForAccounts(Database db) async {
+ const bitcoinWallets = 'SELECT walletInfoId FROM WalletInfo WHERE "type" = ?';
+ final btc = WalletType.bitcoin.index;
+
+ // Lightning: 0 -> -2 (first, so accountIndex 0 is free for the Bitcoin card)
+ await db.rawUpdate(
+ "UPDATE OR REPLACE BalanceCardStyleSettings SET accountIndex = -2 "
+ "WHERE accountIndex = 0 AND walletInfoId IN ($bitcoinWallets)",
+ [btc],
+ );
+
+ // Bitcoin: -1 -> 0 (primary account, first in order)
+ await db.rawUpdate(
+ "UPDATE OR REPLACE BalanceCardStyleSettings SET accountIndex = 0, cardOrder = 0 "
+ "WHERE accountIndex = -1 AND walletInfoId IN ($bitcoinWallets)",
+ [btc],
+ );
+}
+
Future<Map<String, dynamic>> dumpDb() async {
try {
return await _dumpDb();
### cw_core/lib/wallet_base.dart
@@ -142,6 +142,10 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
bool get hasLightningSupport => false;
bool get hasSilentPaymentsScanning => false;
+ bool get hasAccountsSupport => false;
+ bool get hasNativeAccounts => false;
+ bool get canToggleMultiAccounts => hasAccountsSupport && !hasNativeAccounts;
+
// hardware wallet - bitbox, ledger, trezor.
// we also have airgap wallets but those can't sign messages
bool get canSignMessages => walletInfo.hardwareWalletType == null || walletInfo.isHardwareWallet;
### cw_core/lib/wallet_info.dart
@@ -244,6 +244,68 @@ class WalletInfoAddress {
}
}
+class WalletInfoAccount {
+ WalletInfoAccount({
+ required this.walletInfoId,
+ required this.accountIndex,
+ required this.label,
+ });
+
+ factory WalletInfoAccount.fromJson(Map<String, dynamic> json) => WalletInfoAccount(
+ walletInfoId: json["walletInfoId"] as int,
+ accountIndex: json["accountIndex"] as int,
+ label: json["label"] as String,
+ );
+
+ int walletInfoId;
+ int accountIndex;
+ String label;
+
+ static String get tableName => "walletInfoAccount";
+
+ static Future<List<WalletInfoAccount>> selectList(int walletInfoId) async {
+ final query = await db!.query(
+ tableName,
+ where: "walletInfoId = ?",
+ whereArgs: [walletInfoId],
+ orderBy: "accountIndex ASC",
+ );
+
+ return List.generate(query.length, (index) => WalletInfoAccount.fromJson(query[index]));
+ }
+
+ static Future<int> deleteByWalletInfoId(int walletInfoId) async =>
+ await db!.delete(tableName, where: "walletInfoId = ?", whereArgs: [walletInfoId]);
+
+ static Future<int> insertOrUpdate({
+ required int walletInfoId,
+ required int accountIndex,
+ required String label,
+ }) async =>
+ await db!.transaction((txn) async {
+ final updated = await txn.update(
+ tableName,
+ {"label": label},
+ where: "walletInfoId = ? AND accountIndex = ?",
+ whereArgs: [walletInfoId, accountIndex],
+ );
+
+ if (updated > 0) return updated;
+
+ return await txn.insert(tableName, {
+ "walletInfoId": walletInfoId,
+ "accountIndex": accountIndex,
+ "label": label,
+ });
+ });
+
+ Map<String, dynamic> toJson() => {
+ "walletInfoId": walletInfoId,
+ "accountIndex": accountIndex,
+ "label": label,
+ };
+}
+
class DerivationInfo {
DerivationInfo({
this.id = 0,
@@ -346,60 +408,73 @@ class WalletInfo {
this.hashedWalletIdentifier,
this.isNonSeedWallet,
this.sortOrder,
+ this.currentAccountIndex,
this.addressPageType,
this.receiveInfoboxDismissed,
this.showCombinedBalance,
this.favoriteTokenAddress,
this.showSeedBackupReminder)
: _yatLastUsedAddressController = StreamController<String>.broadcast();
- factory WalletInfo.external(
- {required String id,
- required String name,
- required WalletType type,
- required bool isRecovery,
- required int restoreHeight,
- required DateTime date,
- required String dirPath,
- required String path,
- required String address,
- bool? showIntroCakePayCard,
- String yatEid = '',
- String yatLastUsedAddressRaw = '',
- int? derivationInfoId,
- HardwareWalletType? hardwareWalletType,
- String? parentAddress,
- String? hashedWalletIdentifier,
- bool? isNonSeedWallet,
- int? sortOrder,
- bool? receiveInfoboxDismissed,
- bool? showCombinedBalance,
- String? favoriteTokenAddress}) {
- return WalletInfo(
- 0,
- id,
- name,
- type,
- isRecovery,
- restoreHeight,
- date.millisecondsSinceEpoch,
- dirPath,
- path,
- address,
- yatEid,
- yatLastUsedAddressRaw,
- showIntroCakePayCard,
- derivationInfoId ?? -1,
- hardwareWalletType,
- parentAddress,
- hashedWalletIdentifier,
- isNonSeedWallet ?? false,
- sortOrder ?? 0,
- null,
- receiveInfoboxDismissed ?? false,
- showCombinedBalance ?? true,
- favoriteTokenAddress,
- false);
+ factory WalletInfo.external({
+ required String id,
+ required String name,
+ required WalletType type,
+ required bool isRecovery,
+ required int restoreHeight,
+ required DateTime date,
+ required String dirPath,
+ required String path,
+ required String address,
+ bool? showIntroCakePayCard,
+ String yatEid = "",
+ String yatLastUsedAddressRaw = "",
+ int? derivationInfoId,
+ HardwareWalletType? hardwareWalletType,
+ String? parentAddress,
+ String? hashedWalletIdentifier,
+ bool? isNonSeedWallet,
+ int? sortOrder,
+ int currentAccountIndex = 0,
+ bool? receiveInfoboxDismissed,
+ bool? showCombinedBalance,
+ String? favoriteTokenAddress,
+ }) {
+ final wi = WalletInfo(
+ 0,
+ id,
+ name,
+ type,
+ isRecovery,
+ restoreHeight,
+ date.millisecondsSinceEpoch,
+ dirPath,
+ path,
+ address,
+ yatEid.isNotEmpty ? yatEid : null,
+ yatLastUsedAddressRaw.isNotEmpty ? yatLastUsedAddressRaw : null,
+ showIntroCakePayCard ?? false,
+ derivationInfoId ?? -1,
+ hardwareWalletType,
+ parentAddress,
+ hashedWalletIdentifier,
+ isNonSeedWallet ?? false,
+ sortOrder ?? 0,
+ currentAccountIndex,
+ null,
+ receiveInfoboxDismissed ?? false,
+ showCombinedBalance ?? true,
+ favoriteTokenAddress?.isNotEmpty == true ? favoriteTokenAddress : null,
+ false,
+ );
+
+ if (type == WalletType.bitcoin) {
+ wi.isMultiAccountsEnabled = false;
+ } else if (type == WalletType.monero || type == WalletType.wownero) {
+ wi.isMultiAccountsEnabled = true;
+ }
+
+ return wi;
}
static String get tableName => 'walletInfo';
@@ -408,6 +483,8 @@ class WalletInfo {
int internalId;
+ int currentAccountIndex;
+
String id;
String name;
WalletType type;
@@ -506,8 +583,76 @@ class WalletInfo {
await WalletInfoAddress.insert(internalId, type, address);
}
+ Future<List<WalletInfoAccount>> getAccounts() async {
+ var accounts = await WalletInfoAccount.selectList(internalId);
+
+ if (accounts.isEmpty) {
+ await WalletInfoAccount.insertOrUpdate(
+ walletInfoId: internalId,
+ accountIndex: 0,
+ label: "Primary account",
+ );
+
+ accounts = await WalletInfoAccount.selectList(internalId);
+ }
+
+ if (!accounts.any((a) => a.accountIndex == currentAccountIndex)) {
+ currentAccountIndex = accounts.first.accountIndex;
+ await save();
+ }
+
+ return accounts;
+ }
+
+ Future<void> setSelectedAccount(int accountIndex) async {
+ currentAccountIndex = accountIndex;
+ await save();
+ }
+
+ Future<void> setAccounts(List<WalletInfoAccount> accounts) async {
+ await WalletInfoAccount.deleteByWalletInfoId(internalId);
+
+ for (final account in accounts) {
+ await WalletInfoAccount.insertOrUpdate(
+ walletInfoId: internalId,
+ accountIndex: account.accountIndex,
+ label: account.label,
+ );
+ }
+
+ if (accounts.isNotEmpty && !accounts.any((a) => a.accountIndex == currentAccountIndex)) {
+ currentAccountIndex = accounts.first.accountIndex;
+ await save();
+ }
+ }
+
+ Future<void> addAccount({
+ required int accountIndex,
+ required String label,
+ }) async {
+ await WalletInfoAccount.insertOrUpdate(
+ walletInfoId: internalId,
+ accountIndex: accountIndex,
+ label: label,
+ );
+ }
+
+ Future<void> renameAccount({
+ required int accountIndex,
+ required String label,
+ }) async {
+ await WalletInfoAccount.insertOrUpdate(
+ walletInfoId: internalId,
+ accountIndex: accountIndex,
+ label: label,
+ );
+ }
+
String? addressPageType;
String? network;
+ int? accountDiscoveryLimit;
+ bool? isMultiAccountsEnabled;
+
int derivationInfoId;
DerivationInfo? _derivationInfo;
@@ -586,12 +731,16 @@ class WalletInfo {
"hashedWalletIdentifier": hashedWalletIdentifier,
"isNonSeedWallet": isNonSeedWallet ? 1 : 0,
"sortOrder": sortOrder,
+ "currentAccountIndex": currentAccountIndex,
"addressPageType": addressPageType,
"receiveInfoboxDismissed": receiveInfoboxDismissed ? 1 : 0,
"showCombinedBalance": showCombinedBalance ? 1 : 0,
"favoriteTokenAddress": favoriteTokenAddress,
"showSeedBackupReminder": showSeedBackupReminder ? 1 : 0,
"network": network,
+ "accountDiscoveryLimit": accountDiscoveryLimit,
+ "isMultiAccountsEnabled":
+ isMultiAccountsEnabled == null ? null : (isMultiAccountsEnabled! ? 1 : 0),
};
factory WalletInfo.fromJson(Map<String, dynamic> json) {
@@ -617,12 +766,17 @@ class WalletInfo {
json['hashedWalletIdentifier'] as String?,
(json['isNonSeedWallet'] as int) == 1,
json['sortOrder'] as int? ?? 0,
+ json['currentAccountIndex'] as int? ?? 0,
json['addressPageType'] as String? ?? null,
json['receiveInfoboxDismissed'] != 0,
json["showCombinedBalance"] != 0,
json["favoriteTokenAddress"] as String? ?? null,
json["showSeedBackupReminder"] == 1);
info.network = json['network'] as String?;
+ info.accountDiscoveryLimit = json['accountDiscoveryLimit'] as int?;
+ final rawIsMultiAccountsEnabled = json['isMultiAccountsEnabled'];
+ info.isMultiAccountsEnabled =
+ rawIsMultiAccountsEnabled == null ? null : (rawIsMultiAccountsEnabled as int) == 1;
return info;
}
@@ -670,6 +824,11 @@ class WalletInfo {
await save();
}
+ Future<void> setAccountDiscoveryLimit(int limit) async {
+ accountDiscoveryLimit = limit;
+ await save();
+ }
+
Future<void> clearSeedBackupReminder() async {
final previous = showSeedBackupReminder;
showSeedBackupReminder = false;
### cw_core/lib/wallet_info_legacy.dart
@@ -271,6 +271,7 @@ class WalletInfo extends HiveObject {
hashedWalletIdentifier,
isNonSeedWallet,
0,
+ 0,
addressPageType,
false,
true,
### cw_dogecoin/lib/src/dogecoin_wallet.dart
@@ -54,8 +54,10 @@ abstract class DogeCoinWalletBase extends ElectrumWallet with Store {
initialAddresses: initialAddresses,
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
- mainHdByType: mainHdByType,
- sideHdByType: sideHdByType,
+ mainHdByTypeAndAccount: mainHdByTypeAndAccount,
+ sideHdByTypeAndAccount: sideHdByTypeAndAccount,
+ accountIndexes: [currentAccountIndex],
+ currentAccountIndex: currentAccountIndex,
legacyMainHd: mainHd,
legacySideHd: sideHd,
network: network,
### cw_dogecoin/lib/src/dogecoin_wallet_addresses.dart
@@ -13,8 +13,10 @@ class DogeCoinWalletAddresses = DogeCoinWalletAddressesBase with _$DogeCoinWalle
abstract class DogeCoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
DogeCoinWalletAddressesBase(
WalletInfo walletInfo, {
- required super.mainHdByType,
- required super.sideHdByType,
+ required super.mainHdByTypeAndAccount,
+ required super.sideHdByTypeAndAccount,
+ required super.accountIndexes,
+ required super.currentAccountIndex,
required super.legacyMainHd,
required super.legacySideHd,
required super.network,
### cw_monero/lib/monero_wallet.dart
@@ -120,6 +120,12 @@ abstract class MoneroWalletBase
@override
bool get hasRescan => true;
+ @override
+ bool get hasAccountsSupport => true;
+
+ @override
+ bool get hasNativeAccounts => true;
+
@override
String get seed => monero_wallet.getSeed();
String seedLegacy(String? language) => monero_wallet.getSeedLegacy(language);
### cw_wownero/lib/wownero_wallet.dart
@@ -137,6 +137,9 @@ abstract class WowneroWalletBase
@override
bool get hasRescan => true;
+ @override
+ bool get hasAccountsSupport => true;
+
@override
MoneroWalletKeys get keys => MoneroWalletKeys(
primaryAddress: wownero_wallet.getAddress(accountIndex: 0, addressIndex: 0),
### lib/bitcoin/cw_bitcoin.dart
@@ -256,7 +256,10 @@ class CWBitcoin extends Bitcoin {
List<BitcoinUnspent> getUnspents(Object wallet,
{UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) {
final bitcoinWallet = wallet as ElectrumWallet;
- return bitcoinWallet.unspentCoins.where((element) {
+
+ final coins = bitcoinWallet.unspentCoinsForCurrentAccount;
+
+ return coins.where((element) {
switch (coinTypeToSpendFrom) {
case UnspentCoinType.mweb:
return element.bitcoinAddressRecord.type == SegwitAddresType.mweb;
@@ -919,4 +922,36 @@ class CWBitcoin extends Bitcoin {
}
return null;
}
+
+ @override
+ ElectrumBalance balanceForAccount(Object wallet, int accountIndex) {
+ final bitcoinWallet = wallet as ElectrumWallet;
+ return bitcoinWallet.balanceForAccount(accountIndex);
+ }
+
+ @override
+ Map<int, Object> accountBalancesSnapshot(Object wallet) {
+ final bitcoinWallet = wallet as ElectrumWallet;
+ return Map<int, Object>.of(bitcoinWallet.accountBalances);
+ }
+
+ @override
+ List<TransactionInfo> getCurrentAccountBitcoinTransactions(Object wallet) =>
+ (wallet as ElectrumWallet).currentAccountBitcoinTransactions;
+
+ @override
+ Future<void> setCurrentAccount(Object wallet, int accountIndex) async {
+ final bitcoinWallet = wallet as ElectrumWallet;
+ await bitcoinWallet.setCurrentAccount(accountIndex);
+ }
+
+ @override
+ Future<Account> getCurrentAccount(Object wallet) async {
+ final electrumWallet = wallet as ElectrumWallet;
+ final acc = await electrumWallet.loadCurrentAccount();
+ return Account(
+ id: acc.accountIndex,
+ label: acc.label,
+ balance: electrumWallet.balanceForAccount(acc.accountIndex).confirmed.toString());
+ }
}
### lib/di.dart
@@ -55,7 +55,6 @@ import 'package:cake_wallet/nano/nano.dart';
import 'package:cake_wallet/new-ui/model/charts/price_store.dart';
import 'package:cake_wallet/new-ui/new_dashboard.dart';
import 'package:cake_wallet/new-ui/pages/about_page.dart';
-import 'package:cake_wallet/new-ui/pages/account_customizer.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_amount_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_receiving_wallet_page.dart';
@@ -131,6 +130,7 @@ import 'package:cake_wallet/src/screens/settings/privacy_page.dart';
import 'package:cake_wallet/src/screens/settings/security_backup_page.dart';
import 'package:cake_wallet/src/screens/settings/silent_payments_logs_page.dart';
import 'package:cake_wallet/src/screens/settings/silent_payments_settings.dart';
+import 'package:cake_wallet/new-ui/pages/wallet_accounts_page.dart';
import 'package:cake_wallet/src/screens/setup_2fa/modify_2fa_page.dart';
import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa.dart';
import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart';
@@ -214,11 +214,14 @@ import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart';
import 'package:cake_wallet/view_model/integrations/deuro_view_model.dart';
import 'package:cake_wallet/view_model/link_view_model.dart';
-import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
-import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
-import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
-import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart';
-import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart';
+import "package:cake_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart";
+import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
+import 'package:cake_wallet/view_model/wallet_account_list/bitcoin_account_list/bitcoin_account_edit_or_create_view_model.dart';
+import 'package:cake_wallet/view_model/wallet_account_list/bitcoin_account_list/bitcoin_account_list_view_model.dart';
+import 'package:cake_wallet/view_model/wallet_account_list/monero_account_list/monero_account_edit_or_create_view_model.dart';
+import 'package:cake_wallet/view_model/wallet_account_list/monero_account_list/monero_account_list_view_model.dart';
+import 'package:cake_wallet/view_model/wallet_account_list/nano_account_list/nano_account_edit_or_create_view_model.dart';
+import 'package:cake_wallet/view_model/wallet_account_list/nano_account_list/nano_account_list_view_model.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
import 'package:cake_wallet/view_model/order_details_view_model.dart';
@@ -248,6 +251,7 @@ import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_details_view_
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart';
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
import 'package:cake_wallet/view_model/bridge/bridge_view_model.dart';
+import 'package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
@@ -290,6 +294,9 @@ import 'buy/meld/meld_buy_provider.dart';
import 'dogecoin/dogecoin.dart';
import 'new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
import 'new-ui/widgets/addresses_page/address_info.dart';
+import 'src/screens/buy/buy_sell_page.dart';
+import "src/screens/settings/widgets/account_creation_modal.dart";
+
final getIt = GetIt.instance;
@@ -535,6 +542,13 @@ Future<void> setup({
getIt.registerFactory(() => DashboardViewModel(
tradeMonitor: getIt.get<TradeMonitor>(),
balanceViewModel: getIt.get<BalanceViewModel>(),
+ accountListViewModelFactory: () {
+ final wallet = getIt.get<AppStore>().wallet;
+ if (wallet == null) return null;
+ final hasAccounts = wallet.hasAccountsSupport;
+ if (!hasAccounts) return null;
+ return getIt.get<WalletAccountListViewModel>();
+ },
appStore: getIt.get<AppStore>(),
tradesStore: getIt.get<TradesStore>(),
ordersStore: getIt.get<OrdersStore>(),
@@ -560,21 +574,11 @@ Future<void> setup({
getIt.registerLazySingleton<PriceStore>(() => PriceStore());
getIt.registerFactory<ChartsBloc>(
- () => ChartsBloc(appStore: getIt.get<AppStore>(), priceStore: getIt.get<PriceStore>()));
+ () => ChartsBloc(appStore: getIt.get<AppStore>(), priceStore: getIt.get<PriceStore>()));
getIt.registerFactory<ChartsPage>(() => ChartsPage(
- chartsBloc: getIt.get<ChartsBloc>(),
- ));
-
- getIt.registerFactory<AccountCreationModal>(() => AccountCreationModal(
- accountEditOrCreateViewModel: getIt.get<MoneroAccountEditOrCreateViewModel>()));
-
- getIt.registerFactoryParam<AccountCustomizer, DashboardViewModel, void>(
- (dashboardViewModel, _) => AccountCustomizer(
- accountListViewModel: getIt.get<MoneroAccountListViewModel>(),
- dashboardViewModel: dashboardViewModel,
- ),
- );
+ chartsBloc: getIt.get<ChartsBloc>(),
+ ));
getIt.registerFactory<LightningUsernameBloc>(
() => LightningUsernameBloc(getIt.get<AppStore>().wallet!));
@@ -898,13 +902,43 @@ Future<void> setup({
'Unexpected wallet type: ${wallet.type} for generate Monero AccountListViewModel');
});
+ getIt.registerFactory<BitcoinAccountListViewModel>(() {
+ final wallet = getIt.get<AppStore>().wallet!;
+ if (wallet.type == WalletType.bitcoin) {
+ return BitcoinAccountListViewModel(wallet, getIt.get<SettingsStore>());
+ }
+ throw Exception(
+ 'Unexpected wallet type: ${wallet.type} for generate Bitcoin AccountListViewModel');
+ });
+
+ getIt.registerFactory<WalletAccountListViewModel>(() {
+ final wallet = getIt.get<AppStore>().wallet!;
+
+ switch (wallet.type) {
+ case WalletType.bitcoin:
+ return getIt.get<BitcoinAccountListViewModel>();
+ case WalletType.monero:
+ case WalletType.wownero:
+ case WalletType.haven:
+ return getIt.get<MoneroAccountListViewModel>();
+ default:
+ throw Exception('Unsupported wallet type for WalletAccountListViewModel: ${wallet.type}');
+ }
+ });
+
getIt.registerFactoryParam<MoneroAccountEditOrCreateViewModel, AccountListItem?, void>(
(AccountListItem? account, _) => MoneroAccountEditOrCreateViewModel(
monero!.getAccountList(getIt.get<AppStore>().wallet!),
wownero?.getAccountList(getIt.get<AppStore>().wallet!),
wallet: getIt.get<AppStore>().wallet!,
accountListItem: account));
+ getIt.registerFactory<BitcoinAccountEditOrCreateViewModel>(
+ () => BitcoinAccountEditOrCreateViewModel(
+ wallet: getIt.get<AppStore>().wallet!,
+ ),
+ );
+
getIt.registerFactoryParam<NanoAccountEditOrCreateViewModel, NanoAccount?, void>(
(NanoAccount? account, _) =>
NanoAccountEditOrCreateViewModel(nano!.getAccountList(getIt.get<AppStore>().wallet!),
@@ -1002,6 +1036,31 @@ Future<void> setup({
getIt.registerFactory(() => SecurityBackupPage(getIt.get<SecuritySettingsViewModel>(),
getIt.get<AuthService>(), getIt.get<AppStore>().wallet!.isHardwareWallet));
+
+ getIt.registerFactory<WalletAccountEditOrCreateViewModel>(() {
+ final wallet = getIt.get<AppStore>().wallet!;
+ switch (wallet.type) {
+ case WalletType.bitcoin:
+ return getIt.get<BitcoinAccountEditOrCreateViewModel>();
+ case WalletType.monero:
+ case WalletType.wownero:
+ case WalletType.haven:
+ return getIt.get<MoneroAccountEditOrCreateViewModel>();
+ default:
+ throw Exception(
+ 'Unsupported wallet type for WalletAccountEditOrCreateViewModel: ${wallet.type}');
+ }
+ });
+
+ getIt.registerFactoryParam<WalletAccountsPage, DashboardViewModel, void>(
+ (dashboardViewModel, _) => WalletAccountsPage(
+ dashboardViewModel: dashboardViewModel,
+ accountListViewModel:
+ dashboardViewModel.accountListViewModel ?? getIt.get<WalletAccountListViewModel>(),
+ accountEditOrCreateViewModel: getIt.get<WalletAccountEditOrCreateViewModel>(),
+ ),
+ );
+
getIt.registerFactory(() => PrivacyPage(getIt.get<PrivacySettingsViewModel>()));
### lib/entities/preferences_key.dart
@@ -117,7 +117,6 @@ class PreferencesKey {
static const showDecredInfoCard = 'show_decred_info_card';
static const forceDecentralizedExchanges = 'force_decentralized_exchanges';
static const decentralizedExchangesPromptDismissed = 'decentralized_exchanges_prompt_dismissed';
-
static String moneroWalletUpdateV1Key(String name) =>
'${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';
### lib/new-ui/pages/account_customizer.dart
@@ -1,505 +0,0 @@
-import 'dart:ui';
-import 'package:cake_wallet/di.dart';
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/new-ui/pages/card_customizer.dart';
-import 'package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
-import 'package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart';
-import 'package:cake_wallet/new-ui/widgets/modal_grab_handle.dart';
-import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
-import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
-import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
-import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
-import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
-import 'package:cake_wallet/utils/show_pop_up.dart';
-import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
-import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
-import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
-import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
-import 'package:cw_core/balance_card_style_settings.dart';
-import 'package:cw_core/card_design.dart';
-import 'package:cw_core/generate_name.dart';
-import 'package:cw_core/sync_status.dart';
-import 'package:cw_core/utils/print_verbose.dart';
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
-
-class AccountCustomizerListItem {
- final BalanceCard card;
- final int order;
- final AccountListItem accountListItem;
-
- AccountCustomizerListItem(
- {required this.card, required this.order, required this.accountListItem});
-}
-
-class AccountCustomizer extends StatefulWidget {
- const AccountCustomizer(
- {super.key, required this.accountListViewModel, required this.dashboardViewModel});
-
- final MoneroAccountListViewModel accountListViewModel;
- final DashboardViewModel dashboardViewModel;
-
- @override
- State<AccountCustomizer> createState() => _AccountCustomizerState();
-}
-
-class _AccountCustomizerState extends State<AccountCustomizer> {
- static const double _kStackVisibleFactor = 0.2;
- late final double cardWidth = MediaQuery.of(context).size.width * 0.9;
-
- final List<AccountCustomizerListItem> _items = [];
-
- @override
- void initState() {
- super.initState();
- WidgetsBinding.instance.addPostFrameCallback((_) {
- loadCards();
- final activeId = widget.accountListViewModel.selected.id;
- for (int i = 0; i < _items.length - 1; i++) {
- if (_items[i].accountListItem.id == activeId) {
- final lastIndex = _items.length - 1;
- final temp = _items[i];
- _items[i] = _items[lastIndex];
- _items[lastIndex] = temp;
- saveCardOrder();
- widget.dashboardViewModel.loadCardDesigns();
- break;
- }
- }
- });
- }
-
- @override
- void dispose() {
- saveCardOrder().then((value) => widget.dashboardViewModel.loadCardDesigns());
- super.dispose();
- }
-
- void loadCards() {
- _items.clear();
-
- final accounts = widget.accountListViewModel.accounts;
- for (int i = 0; i < accounts.length; i++) {
- final index = widget.dashboardViewModel.cardOrder[i];
-
- if (index == null || index >= accounts.length) {
- // db order broken.
- reset();
- break;
- }
-
- _items.add(AccountCustomizerListItem(
- card: BalanceCard(
- accountName: accounts[index].label,
- accountIndex: accounts[index].id,
- balance: accounts[index].balance ?? "0.00",
- accountBalance: accounts[index].balance ?? "0.00",
- designSwitchDuration: Duration.zero,
- assetName: widget.accountListViewModel.currency.title,
- onCustomizeTapped: (i == accounts.length - 1) ? _openCardCustomizer : null,
- selected: i == accounts.length - 1,
- width: cardWidth,
- design: widget.dashboardViewModel.cardDesigns[index],
- ),
- order: index,
- accountListItem: accounts[index]));
- }
- setState(() {});
- }
-
- @override
- Widget build(BuildContext context) {
- if (_items.isEmpty) return SizedBox.shrink();
-
- return Container(
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.surface,
- borderRadius: BorderRadius.vertical(top: Radius.circular(24))),
- child: Column(
- children: [
- ModalTopBar(
- title: S.of(context).wallet_accounts,
- leadingIcon: Icon(Icons.close),
- leadingSemanticLabel: S.of(context).close,
- onLeadingPressed: Navigator.of(context).maybePop,
- trailingIcon: Icon(Icons.refresh),
- trailingSemanticLabel: S.of(context).reset,
- onTrailingPressed: showResetDialog,
- ),
- Padding(
- padding: const EdgeInsets.symmetric(vertical: 24.0),
- child: Text(
- S.of(context).account_customizer_desc,
- style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
- ),
- ),
- Expanded(
- child: Stack(
- children: [
- ReorderableListView.builder(
- padding: EdgeInsets.only(bottom: 196),
- scrollController: ModalScrollController.of(context),
- onReorder: reorder,
- proxyDecorator: (child, index, animation) {
- return AnimatedBuilder(
- animation: animation,
- builder: (context, _) {
- final animValue = Curves.easeOutCubic.transform(animation.value);
- final scale = lerpDouble(1, 1.05, animValue)!;
-
- return Opacity(
- opacity: 1 - animValue.clamp(0.0, 0.1),
- child: Center(
- child: SizedBox(
- width: cardWidth,
- child: Transform.scale(
- scale: scale,
- child: child,
- ),
- ),
- ),
- );
- },
- child: _items[index].card,
- );
- },
- itemCount: _items.length,
- itemBuilder: (BuildContext context, int index) {
- final card = _items[index].card;
- // The stack is ordered bottom to top, so the last item
- // is the account currently in front — the selected one.
- final selectedItemIndex = _items.length - 1;
-
- return Container(
- key: ValueKey(index),
- // One labeled, selectable node per account; the card's own
- // texts stay reachable underneath it.
- child: Semantics(
- button: true,
- selected: selectedItemIndex == index,
- label: _items[index].accountListItem.label,
- onTap: () => reorder(index, _items.length),
- child: GestureDetector(
- excludeFromSemantics: true,
- onTap: () {
- reorder(index, _items.length);
- },
- child: Align(
- alignment: Alignment.topCenter,
- heightFactor: _kStackVisibleFactor,
- child: card,
- ),
- ),
- ),
- );
- },
- ),
- SafeArea(
- child: Padding(
- padding: EdgeInsets.only(bottom: 50),
- child: Align(
- alignment: Alignment.bottomCenter,
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 24.0),
- child: Material(
- color: Colors.transparent,
- child: MergeSemantics(
- child: Semantics(
- button: true,
- child: InkWell(
- borderRadius: BorderRadius.circular(999999),
- onTap: _showAddAccountModal,
- child: Container(
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.surfaceContainer,
- borderRadius: BorderRadius.circular(999999)),
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 18.0),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- spacing: 8,
- children: [
- Icon(
- Icons.add,
- size: 28,
- color: Theme.of(context).colorScheme.primary,
- ),
- Text(
- S.of(context).add_account,
- style: TextStyle(
- color: Theme.of(context).colorScheme.primary,
- fontWeight: FontWeight.w500),
- )
- ],
- ),
- ),
- ),
- ),
- ),
- ),
- ),
- )))),
- ],
- ),
- ),
- ],
- ),
- );
- }
-
- bool _checkReadyToManage() {
- if (widget.dashboardViewModel.status is! SyncedSyncStatus) {
- showDialog(
- context: context,
- builder: (context) => AlertWithOneAction(
- alertTitle: S.of(context).wallet_is_syncing,
- alertContent: S.of(context).cannot_manage_accounts_during_sync,
- buttonText: S.of(context).ok,
- buttonAction: Navigator.of(context).pop));
- return false;
- }
- return true;
- }
-
- Future<void> _showAddAccountModal() async {
- if (!_checkReadyToManage()) {
- return;
- }
-
- final res = await showCupertinoModalBottomSheet(
- context: context,
- backgroundColor: Colors.transparent,
- builder: (context) {
- final modal = getIt.get<AccountCreationModal>();
- return Material(child: modal);
- });
- if (res != null && res is bool && res == true) {
- await widget.dashboardViewModel.loadCardDesigns();
- loadCards();
- await saveCardOrder();
- }
- }
-
- void _openCardCustomizer() {
- if (!_checkReadyToManage()) {
- return;
- }
-
- final bloc = getIt.get<CardCustomizerBloc>(param1: false);
-
- Navigator.of(context).push(CupertinoPageRoute(
- builder: (context) {
- return BlocProvider(
- create: (context) => bloc,
- child: Material(
- child: CardCustomizer(
- cryptoTitle: widget.dashboardViewModel.wallet.currency.fullName ??
- widget.dashboardViewModel.wallet.currency.name,
- cryptoName: widget.dashboardViewModel.wallet.currency.name,
- ),
- ),
- );
- },
- )).then((_) async {
- bloc.add(DesignSaved());
- await bloc.stream.firstWhere((item) => item is CardCustomizerSaved);
- await widget.dashboardViewModel.loadCardDesigns();
- loadCards();
- });
- }
-
- void reorder(int oldIndex, int newIndex) {
- setState(() {
- if (oldIndex < newIndex) {
- newIndex -= 1;
- }
- final AccountCustomizerListItem item = _items.removeAt(oldIndex);
- _items.insert(newIndex, item);
- });
-
- // necessary to copy all this to keep constant constructor for BalanceCard
- for (int i = 0; i < _items.length; i++) {
- _items[i] = AccountCustomizerListItem(
- card: BalanceCard(
- accountName: _items[i].card.accountName,
- balance: _items[i].card.balance,
- accountIndex: _items[i].card.accountIndex,
- accountBalance: _items[i].card.accountBalance,
- assetName: _items[i].card.assetName,
- designSwitchDuration: _items[i].card.designSwitchDuration,
- onCustomizeTapped: (i == _items.length - 1) ? _openCardCustomizer : null,
- selected: i == _items.length - 1,
- width: _items[i].card.width,
- design: _items[i].card.design,
- ),
- order: i,
- accountListItem: _items[i].accountListItem);
- }
-
- if (newIndex == _items.length - 1 || oldIndex == _items.length - 1) {
- widget.accountListViewModel.select(_items[_items.length - 1].accountListItem);
- }
- }
-
- Future<void> saveCardOrder() async {
- for (int i = 0; i < _items.length; i++) {
- final idx = _items.indexWhere((element) => element.accountListItem.id == i);
- printV("$i: $idx");
-
- await BalanceCardStyleSettings.fromCardDesign(
- walletInfoId: widget.dashboardViewModel.wallet.walletInfo.internalId,
- accountIndex: i,
- cardOrder: idx,
- design: _items[idx].card.design)
- .insert();
- }
- }
-
- Future<void> showResetDialog() async {
- final res = await showPopUp(
- context: context,
- builder: (context) {
- return AlertWithTwoActions(
- alertTitle: S.of(context).reset,
- alertContent: S.of(context).card_order_reset_desc,
- leftButtonText: S.of(context).yes,
- rightButtonText: S.of(context).no,
- actionLeftButton: () {
- Navigator.of(context).pop(true);
- },
- actionRightButton: Navigator.of(context).pop);
- });
- if (res != null && res is bool && res) {
- reset();
- }
- }
-
- Future<void> reset() async {
- _items.clear();
-
- final accounts = widget.accountListViewModel.accounts;
- for (int i = 0; i < widget.accountListViewModel.accounts.length; i++) {
- _items.add(AccountCustomizerListItem(
- card: BalanceCard(
- accountName: accounts[i].label,
- accountIndex: accounts[i].id,
- balance: accounts[i].balance ?? "0.00",
- accountBalance: accounts[i].balance ?? "0.00",
- assetName: widget.accountListViewModel.currency.title,
- selected: true,
- designSwitchDuration: Duration(milliseconds: 200),
- width: cardWidth,
- design: i >= widget.dashboardViewModel.cardDesigns.length
- ? CardDesign.genericDefault
- : widget.dashboardViewModel.cardDesigns[i],
- ),
- order: i,
- accountListItem: accounts[i]));
- }
-
- saveCardOrder();
- setState(() {});
- }
-}
-
-class AccountCreationModal extends StatefulWidget {
- const AccountCreationModal({super.key, required this.accountEditOrCreateViewModel});
-
- final MoneroAccountEditOrCreateViewModel accountEditOrCreateViewModel;
-
- @override
- State<AccountCreationModal> createState() => _AccountCreationModalState();
-}
-
-class _AccountCreationModalState extends State<AccountCreationModal> {
- final TextEditingController _controller = TextEditingController();
- bool _loading = false;
-
- Future<void> _generateAccountName() async => _controller.text = await generateName();
-
- @override
- Widget build(BuildContext context) {
- return Container(
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.surface,
- borderRadius: BorderRadius.vertical(top: Radius.circular(30))),
- child: SafeArea(
- top: false,
- child: Padding(
- padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- ModalGrabHandle(),
- ModalTopBar(title: S.of(context).create_account),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 18.0),
- child: Column(
- spacing: 50,
- children: [
- SizedBox(),
- Container(
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.surfaceContainer,
- borderRadius: BorderRadius.circular(16)),
- child: Row(
- children: [
- Expanded(
- child: TextField(
- controller: _controller,
- decoration: InputDecoration(hintText: S.of(context).account_name),
- ),
- ),
- Padding(
- padding: const EdgeInsets.all(12.0),
- child: Semantics(
- button: true,
- label: S.of(context).generate_name,
- onTap: _generateAccountName,
- child: ExcludeSemantics(
- child: GestureDetector(
- onTap: _generateAccountName,
- child: Container(
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.surfaceContainerHigh,
- borderRadius: BorderRadius.circular(5)),
- child: CakeImageWidget(
- imageUrl: "assets/new-ui/randomize.svg",
- colorFilter: ColorFilter.mode(
- Theme.of(context).colorScheme.primary, BlendMode.srcIn),
- ),
- ),
- ),
- ),
- ),
- )
- ],
- ),
- ),
- NewPrimaryButton(
- onPressed: () async {
- if (_loading) return;
- setState(() {
- _loading = true;
- });
- widget.accountEditOrCreateViewModel.label = _controller.text;
- await widget.accountEditOrCreateViewModel.save();
- Navigator.of(context).pop(true);
- },
- text: S.of(context).continue_text,
- color: Theme.of(context).colorScheme.primary,
- textColor: Theme.of(context).colorScheme.onPrimary,
- isLoading: _loading,
- ),
- SizedBox(),
- ],
- ),
- )
- ],
- ),
- ),
- ));
- }
-}
### lib/new-ui/pages/addresses_page.dart
@@ -87,7 +87,12 @@ class _NewAddressesPageState extends State<NewAddressesPage> {
});
WidgetsBinding.instance.addPostFrameCallback((_) {
- reaction((context) => widget.dashboardViewModel.cardDesigns.first, (value) {
+ reaction((_) {
+ final index = widget.dashboardViewModel.accountListViewModel?.selectedAccount?.id ?? 0;
+ final designs = widget.dashboardViewModel.cardDesigns;
+ return (index >= 0 && index < designs.length) ? designs[index] : null;
+ }, (value) {
+ if (!mounted) return;
setState(() {
design = value;
});
@@ -130,8 +135,7 @@ class _NewAddressesPageState extends State<NewAddressesPage> {
child: Column(
spacing: 16,
children: [
- if (widget.dashboardViewModel.type == WalletType.monero ||
- widget.dashboardViewModel.type == WalletType.wownero)
+ if (widget.dashboardViewModel.isMultiAccountsEnabled)
Observer(
builder: (_) => AccountPreviewHeader(
dashboardViewModel: widget.dashboardViewModel,
@@ -271,8 +275,7 @@ class AccountPreviewHeader extends StatelessWidget {
final CardDesign? design;
@override
- Widget build(BuildContext context) {
- return Container(
+ Widget build(BuildContext context) => Container(
height: 64,
width: MediaQuery.of(context).size.width * 0.9,
decoration: BoxDecoration(
@@ -295,9 +298,7 @@ class AccountPreviewHeader extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
- monero?.getCurrentAccount(dashboardViewModel.wallet).label ??
- wownero?.getCurrentAccount(dashboardViewModel.wallet).label ??
- "",
+ dashboardViewModel.accountListViewModel?.selectedAccount?.label ?? '',
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.primary),
),
Text(
@@ -325,7 +326,6 @@ class AccountPreviewHeader extends StatelessWidget {
),
),
);
- }
}
class AddressRow extends StatelessWidget {
### lib/new-ui/pages/card_customizer.dart
@@ -1,21 +1,77 @@
-import 'dart:async';
-import 'dart:math';
+import "dart:async";
+import "dart:math";
-import 'package:cake_wallet/generated/i18n.dart';
-import 'package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
-import 'package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart';
-import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
-import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
-import 'package:cw_core/card_design.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/generated/i18n.dart";
+import "package:cake_wallet/new-ui/modal_navigator.dart";
+import "package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart";
+import "package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart";
+import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
+import "package:cake_wallet/src/widgets/cake_image_widget.dart";
+import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
+import "package:cw_core/card_design.dart";
+import "package:flutter/material.dart";
+import "package:flutter_bloc/flutter_bloc.dart";
+import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
class CardCustomizer extends StatefulWidget {
- const CardCustomizer({super.key, required this.cryptoTitle, required this.cryptoName});
+ const CardCustomizer({required this.cryptoTitle, required this.cryptoName, super.key});
final String cryptoTitle;
final String cryptoName;
+ static Future<void> show({
+ required BuildContext context,
+ required DashboardViewModel dashboardViewModel,
+ required bool lightningMode,
+ bool asModalSheet = true,
+ VoidCallback? onSaved,
+ }) async {
+ final bloc = getIt.get<CardCustomizerBloc>(
+ param1: lightningMode,
+ param2: dashboardViewModel.settingsStore.displayAmountsInSatoshi,
+ );
+
+ final customizer = BlocProvider(
+ create: (_) => bloc,
+ child: Material(
+ child: CardCustomizer(
+ cryptoTitle: dashboardViewModel.wallet.currency.fullName ??
+ dashboardViewModel.wallet.currency.name,
+ cryptoName: dashboardViewModel.wallet.currency.name,
+ ),
+ ),
+ );
+
+ if (asModalSheet) {
+ await CupertinoScaffold.showCupertinoModalBottomSheet(
+ barrierColor: Colors.black.withAlpha(60),
+ context: context,
+ builder: (context) => ModalNavigator(
+ parentContext: context,
+ heightMode: ModalHeightModes.fullScreen,
+ rootPage: customizer,
+ ),
+ );
+ } else {
+ await Navigator.of(context).push(
+ MaterialPageRoute<void>(builder: (_) => customizer),
+ );
+ }
+
+ bloc.add(DesignSaved());
+
+ await bloc.stream.firstWhere(
+ (state) => state is CardCustomizerSaved,
+ orElse: () => bloc.state,
+ );
+
+ await dashboardViewModel.loadCardDesigns();
+ await dashboardViewModel.accountListViewModel?.reload();
+
+ onSaved?.call();
+ }
+
@override
State<CardCustomizer> createState() => _CardCustomizerState();
}
@@ -63,298 +119,303 @@ class _CardCustomizerState extends State<CardCustomizer> {
}
@override
- Widget build(BuildContext context) {
- return BlocListener<CardCustomizerBloc, CardCustomizerState>(
- listenWhen: (previous, current) => previous.accountName != current.accountName,
- listener: (context, state) {
- if (accountNameController.text != state.accountName) {
- accountNameController.text = state.accountName;
- }
- },
- child: BlocBuilder<CardCustomizerBloc, CardCustomizerState>(
- builder: (context, state) {
- if (state is CardCustomizerNotLoaded) return SizedBox.shrink();
- return PopScope(
- child: SingleChildScrollView(
- child: SafeArea(
- child: Column(
- spacing: 25.0,
- mainAxisSize: MainAxisSize.min,
- children: [
- ModalTopBar(
- title: editEnabled ? S.of(context).edit_account : S.of(context).edit_card,
- leadingIcon: Icon(Icons.close),
- leadingSemanticLabel: S.of(context).close,
- // trailingIcon: editEnabled ? Icon(Icons.delete_forever) : null,
- onLeadingPressed: () => Navigator.of(context).maybePop(),
- // onTrailingPressed: () {},
- ),
- if (editEnabled)
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 18.0),
- child: Column(
- spacing: 8.0,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(S.of(context).account_name),
- TextField(
- maxLength: 32,
- decoration: InputDecoration(counterText: ""),
- onChanged: (value) {
- context.read<CardCustomizerBloc>().add(AccountNameChanged(value));
- },
- controller: accountNameController,
- )
- ],
- ),
+ Widget build(BuildContext context) => BlocListener<CardCustomizerBloc, CardCustomizerState>(
+ listenWhen: (previous, current) => previous.accountName != current.accountName,
+ listener: (context, state) {
+ if (accountNameController.text != state.accountName) {
+ accountNameController.text = state.accountName;
+ }
+ },
+ child: BlocBuilder<CardCustomizerBloc, CardCustomizerState>(
+ builder: (context, state) {
+ if (state is CardCustomizerNotLoaded) return SizedBox.shrink();
+ return PopScope(
+ child: SingleChildScrollView(
+ child: SafeArea(
+ child: Column(
+ spacing: 25.0,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ModalTopBar(
+ title: editEnabled ? S.of(context).edit_account : S.of(context).edit_card,
+ leadingIcon: Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
+ // trailingIcon: editEnabled ? Icon(Icons.delete_forever) : null,
+ onLeadingPressed: () => Navigator.of(context).maybePop(),
+ // onTrailingPressed: () {},
),
- BalanceCard(
- width: min(MediaQuery.of(context).size.width * 0.87, 768),
- selected: true,
- designSwitchDuration: Duration(milliseconds: 300),
- accountName: editEnabled ? state.accountName : "",
- balance: "0.00",
- assetName: state.displaySats ? "sats" : widget.cryptoName,
- capitalizeAssetName: !state.displaySats,
- design: state.selectedDesign,
- ),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 18.0),
- child: Container(
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.surfaceContainer,
- borderRadius: BorderRadius.circular(16),
- ),
+ if (editEnabled)
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Column(
+ spacing: 8.0,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Padding(
- padding: const EdgeInsets.all(12.0),
- child: Column(
- spacing: 8.0,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(S.of(context).card_style),
- Container(
- height: 63,
- child: ListView.separated(
- scrollDirection: Axis.horizontal,
- itemCount: state.availableDesigns.length,
- separatorBuilder: (context, index) {
- return SizedBox(width: 8.0);
- },
- itemBuilder: (context, index) {
- return GestureDetector(
- onTap: () {
- context
- .read<CardCustomizerBloc>()
- .add(CardDesignSelected(index));
- },
- child: AnimatedContainer(
- duration: Duration(milliseconds: 300),
- decoration: ShapeDecoration(
- shape: RoundedSuperellipseBorder(
- side: BorderSide(
- color:
- ((index == state.selectedDesignIndex)
+ Text(S.of(context).account_name),
+ TextField(
+ maxLength: 32,
+ decoration: InputDecoration(counterText: ""),
+ onChanged: (value) {
+ context.read<CardCustomizerBloc>().add(AccountNameChanged(value));
+ },
+ controller: accountNameController,
+ )
+ ],
+ ),
+ ),
+ BalanceCard(
+ width: min(MediaQuery.of(context).size.width * 0.87, 768),
+ selected: true,
+ designSwitchDuration: Duration(milliseconds: 300),
+ accountName: editEnabled ? state.accountName : "",
+ balance: "0.00",
+ assetName: state.displaySats ? "sats" : widget.cryptoName,
+ capitalizeAssetName: !state.displaySats,
+ design: state.selectedDesign,
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 18.0),
+ child: Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.surfaceContainer,
+ borderRadius: BorderRadius.circular(16),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(12.0),
+ child: Column(
+ spacing: 8.0,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(S.of(context).card_style),
+ SizedBox(
+ height: 63,
+ child: ListView.separated(
+ scrollDirection: Axis.horizontal,
+ itemCount: state.availableDesigns.length,
+ separatorBuilder: (context, index) =>
+ const SizedBox(width: 8),
+ itemBuilder: (context, index) => GestureDetector(
+ onTap: () {
+ context
+ .read<CardCustomizerBloc>()
+ .add(CardDesignSelected(index));
+ },
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 300),
+ decoration: ShapeDecoration(
+ shape: RoundedSuperellipseBorder(
+ side: BorderSide(
+ color: ((index ==
+ state.selectedDesignIndex)
? Theme.of(context)
.colorScheme
.onSurface
: Colors.transparent),
- width: 1),
- borderRadius:
- BorderRadiusGeometry.circular(12),
- ),
- ),
- child: AnimatedScale(
- duration: Duration(milliseconds: 200),
- scale: index == state.selectedDesignIndex
- ? 0.94
- : 1,
- child: BalanceCard(
- width: 96,
- borderRadius: 10,
- selected: false,
- designSwitchDuration:
- Duration(milliseconds: 300),
- design: _cardStylePreviewDesign(state, index),
+ width: 1),
+ borderRadius:
+ BorderRadiusGeometry.circular(12),
+ ),
+ ),
+ child: AnimatedScale(
+ duration: const Duration(milliseconds: 200),
+ scale: index == state.selectedDesignIndex
+ ? 0.94
+ : 1,
+ child: BalanceCard(
+ width: 96,
+ borderRadius: 10,
+ selected: false,
+ designSwitchDuration:
+ const Duration(milliseconds: 300),
+ design:
+ _cardStylePreviewDesign(state, index),
+ ),
+ ),
),
- ),
- ),
- );
- }),
- ),
- ],
- )),
- AnimatedSwitcher(
- duration: const Duration(milliseconds: 350),
- switchInCurve: Curves.easeOut,
- switchOutCurve: Curves.easeIn,
- transitionBuilder: (Widget child, Animation<double> animation) {
- return FadeTransition(
+ )),
+ ),
+ ],
+ )),
+ AnimatedSwitcher(
+ duration: const Duration(milliseconds: 350),
+ switchInCurve: Curves.easeOut,
+ switchOutCurve: Curves.easeIn,
+ transitionBuilder: (child, Animation<double> animation) =>
+ FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axisAlignment: -1,
child: child,
),
- );
- },
- child: _showIconStylePanel(state)
- ? Column(
- key: const ValueKey('icon_style_panel'),
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const SizedBox(height: 8),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 12.0),
- child: Column(
- spacing: 8.0,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(S.of(context).icon_style),
- SizedBox(
- height: 48,
- child: ListView.separated(
- scrollDirection: Axis.horizontal,
- itemCount: state.availableIconPaths.length,
- separatorBuilder: (context, _) =>
- const SizedBox(width: 8.0),
- itemBuilder: (context, index) {
- final icon = state.availableIconPaths[index];
- final isSelected =
- index == state.selectedIconIndex;
- return GestureDetector(
- onTap: () {
- context
- .read<CardCustomizerBloc>()
- .add(IconStyleSelected(index));
- },
- child: AnimatedContainer(
- duration:
- const Duration(milliseconds: 200),
- width: 48,
- height: 48,
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(18),
- color: Theme.of(context).brightness ==
- Brightness.light
- ? Theme.of(context)
- .colorScheme
- .onSurfaceVariant
- .withAlpha(64)
- : Theme.of(context)
- .colorScheme
- .surfaceContainerHigh,
- border: Border.all(
- color: isSelected
+ ),
+ child: _showIconStylePanel(state)
+ ? Column(
+ key: const ValueKey("icon_style_panel"),
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 8),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12.0),
+ child: Column(
+ spacing: 8.0,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(S.of(context).icon_style),
+ SizedBox(
+ height: 48,
+ child: ListView.separated(
+ scrollDirection: Axis.horizontal,
+ itemCount: state.availableIconPaths.length,
+ separatorBuilder: (context, _) =>
+ const SizedBox(width: 8.0),
+ itemBuilder: (context, index) {
+ final icon =
+ state.availableIconPaths[index];
+ final isSelected =
+ index == state.selectedIconIndex;
+ return GestureDetector(
+ onTap: () {
+ context
+ .read<CardCustomizerBloc>()
+ .add(IconStyleSelected(index));
+ },
+ child: AnimatedContainer(
+ duration:
+ const Duration(milliseconds: 200),
+ width: 48,
+ height: 48,
+ decoration: BoxDecoration(
+ borderRadius:
+ BorderRadius.circular(18),
+ color: Theme.of(context).brightness ==
+ Brightness.light
? Theme.of(context)
.colorScheme
- .onSurface
- : Colors.transparent,
- width: 2,
+ .onSurfaceVariant
+ .withAlpha(64)
+ : Theme.of(context)
+ .colorScheme
+ .surfaceContainerHigh,
+ border: Border.all(
+ color: isSelected
+ ? Theme.of(context)
+ .colorScheme
+ .onSurface
+ : Colors.transparent,
+ width: 2,
+ ),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(10.0),
+ child: CakeImageWidget(
+ imageUrl: icon.path),
),
),
- child: Padding(
- padding: const EdgeInsets.all(10.0),
- child: CakeImageWidget(
- imageUrl: icon.path),
- ),
- ),
- );
- },
+ );
+ },
+ ),
),
- ),
- ],
+ ],
+ ),
),
- ),
- ],
- )
- : const SizedBox.shrink(key: ValueKey('icon_style_hidden')),
- ),
- SizedBox(height: 8),
- Container(
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.surfaceContainerHigh,
- borderRadius: BorderRadius.circular(16)),
- child: Padding(
- padding: const EdgeInsets.all(12.0),
- child: Column(
- spacing: 8.0,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(S.of(context).color),
- Container(
- width: double.infinity,
- child: Wrap(
- direction: Axis.horizontal,
- spacing: 4, // space between items in a row
- runSpacing: 8,
- children: List.generate(state.availableColors.length,
- (index) {
- return Material(
- borderRadius: BorderRadius.circular(999999999),
- child: InkWell(
- borderRadius: BorderRadius.circular(999999999),
- onTap: () {
- context
- .read<CardCustomizerBloc>()
- .add(ColorSelected(index));
- },
- child: Stack(
- children: [
- AnimatedOpacity(
- duration: Duration(milliseconds: 200),
- opacity: index == state.selectedColorIndex
- ? 1
- : 0,
- child: Container(
- width: 32,
- height: 32,
- decoration: BoxDecoration(
- borderRadius:
- BorderRadius.circular(99999999),
- border: Border.all(
- color: Theme.of(context)
- .colorScheme
- .onSurface))),
- ),
- AnimatedScale(
- duration: Duration(milliseconds: 200),
- scale: index == state.selectedColorIndex
- ? 0.8
- : 1,
- child: Container(
- width: 32,
- height: 32,
- decoration: BoxDecoration(
- borderRadius:
- BorderRadius.circular(99999999),
- gradient:
- state.availableColors[index]),
+ ],
+ )
+ : const SizedBox.shrink(key: ValueKey("icon_style_hidden")),
+ ),
+ const SizedBox(height: 8),
+ Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.surfaceContainerHigh,
+ borderRadius: BorderRadius.circular(16)),
+ child: Padding(
+ padding: const EdgeInsets.all(12.0),
+ child: Column(
+ spacing: 8.0,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(S.of(context).color),
+ Container(
+ width: double.infinity,
+ child: Wrap(
+ direction: Axis.horizontal,
+ spacing: 4, // space between items in a row
+ runSpacing: 8,
+ children: List.generate(
+ state.availableColors.length,
+ (index) => Material(
+ borderRadius:
+ BorderRadius.circular(999999999),
+ child: InkWell(
+ borderRadius:
+ BorderRadius.circular(999999999),
+ onTap: () {
+ context
+ .read<CardCustomizerBloc>()
+ .add(ColorSelected(index));
+ },
+ child: Stack(
+ children: [
+ AnimatedOpacity(
+ duration: const Duration(
+ milliseconds: 200),
+ opacity: index ==
+ state.selectedColorIndex
+ ? 1
+ : 0,
+ child: Container(
+ width: 32,
+ height: 32,
+ decoration: BoxDecoration(
+ borderRadius:
+ BorderRadius.circular(
+ 99999999),
+ border: Border.all(
+ color: Theme.of(context)
+ .colorScheme
+ .onSurface))),
+ ),
+ AnimatedScale(
+ duration: const Duration(
+ milliseconds: 200),
+ scale: index ==
+ state.selectedColorIndex
+ ? 0.8
+ : 1,
+ child: Container(
+ width: 32,
+ height: 32,
+ decoration: BoxDecoration(
+ borderRadius:
+ BorderRadius.circular(
+ 99999999,
+ ),
+ gradient: state
+ .availableColors[index]),
+ ),
+ ),
+ ],
+ ),
),
- ),
- ],
- ),
- ),
- );
- }),
- )),
- ],
+ )),
+ )),
+ ],
+ ),
),
- ),
- )
- ],
- ),
- )),
- ],
+ )
+ ],
+ ),
+ )),
+ ],
+ ),
),
),
- ),
- );
- },
- ),
- );
- }
+ );
+ },
+ ),
+ );
}
### lib/new-ui/pages/home_page.dart
@@ -1,35 +1,40 @@
import "dart:async";
-import 'package:cake_wallet/core/auth_service.dart';
-import 'package:cake_wallet/di.dart';
-import 'package:cake_wallet/generated/i18n.dart';
+import "package:cake_wallet/bitcoin/bitcoin.dart";
+import "package:cake_wallet/core/auth_service.dart";
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/main.dart";
-import 'package:cake_wallet/new-ui/modal_navigator.dart';
-import 'package:cake_wallet/new-ui/pages/account_customizer.dart';
-import 'package:cake_wallet/new-ui/pages/card_customizer.dart';
+import "package:cake_wallet/new-ui/modal_navigator.dart";
+import "package:cake_wallet/new-ui/pages/card_customizer.dart";
import "package:cake_wallet/new-ui/pages/seed/seed_backup_reminder_page.dart";
-import 'package:cake_wallet/new-ui/pages/settings_page.dart';
-import 'package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
-import 'package:cake_wallet/new-ui/widgets/coins_page/action_row/coin_action_row.dart';
-import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/assets_history_section.dart';
-import 'package:cake_wallet/new-ui/widgets/coins_page/cards/cards_view.dart';
-import 'package:cake_wallet/new-ui/widgets/coins_page/mweb_ad.dart';
+import "package:cake_wallet/new-ui/pages/send_page.dart";
+import "package:cake_wallet/new-ui/pages/settings_page.dart";
+import "package:cake_wallet/new-ui/pages/wallet_accounts_page.dart";
+import "package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart";
+import "package:cake_wallet/new-ui/widgets/coins_page/action_row/coin_action_row.dart";
+import "package:cake_wallet/new-ui/widgets/coins_page/assets_history/assets_history_section.dart";
+import "package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart";
+import "package:cake_wallet/new-ui/widgets/coins_page/cards/cards_view.dart";
+import "package:cake_wallet/new-ui/widgets/coins_page/mweb_ad.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/seed_backup_reminder_card.dart";
-import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart';
-import 'package:cake_wallet/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart';
+import "package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart";
+import "package:cake_wallet/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/zcash_migration_modal.dart";
-import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
-import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
-import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:flutter_mobx/flutter_mobx.dart';
-import 'package:mobx/mobx.dart';
-import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
+import "package:cake_wallet/routes.dart";
+import "package:cake_wallet/utils/payment_request.dart";
+import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
+import "package:cake_wallet/view_model/dashboard/nft_view_model.dart";
+import "package:cw_core/unspent_coin_type.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter/cupertino.dart";
+import "package:flutter/material.dart";
+import "package:flutter_mobx/flutter_mobx.dart";
+import "package:mobx/mobx.dart";
+import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
class NewHomePage extends StatefulWidget {
- NewHomePage({super.key, required this.dashboardViewModel, required this.nftViewModel});
+ const NewHomePage({required this.dashboardViewModel, required this.nftViewModel, super.key});
final DashboardViewModel dashboardViewModel;
final NFTViewModel nftViewModel;
@@ -39,20 +44,15 @@ class NewHomePage extends StatefulWidget {
}
class _NewHomePageState extends State<NewHomePage> with RouteAware {
- MoneroAccountListViewModel? accountListViewModel;
- bool _lightningMode = false;
late final ReactionDisposer _walletReaction;
late final ReactionDisposer _migrationReaction;
@override
void initState() {
super.initState();
- _setAccountViewModel();
+
_walletReaction = reaction((_) => widget.dashboardViewModel.wallet, (_) {
- _setAccountViewModel();
- setState(() {
- _lightningMode = false;
- });
+ setState(() {});
});
_migrationReaction = reaction((_) => widget.dashboardViewModel.isMigratingToIronwood, (val) {
@@ -91,178 +91,183 @@ class _NewHomePageState extends State<NewHomePage> with RouteAware {
@override
void didPopNext() => widget.dashboardViewModel.loadSeedBackupReminder();
- void _setAccountViewModel() {
- accountListViewModel = widget.dashboardViewModel.balanceViewModel.hasAccounts
- ? getIt.get<MoneroAccountListViewModel>()
- : null;
- }
-
@override
- Widget build(BuildContext context) {
- return Container(
- height: MediaQuery.of(context).size.height,
- decoration: BoxDecoration(
- gradient: LinearGradient(
- colors: [
- Theme.of(context).colorScheme.surface,
- Theme.of(context).colorScheme.surfaceDim,
- ],
- begin: Alignment.topCenter,
- end: Alignment.bottomCenter,
+ Widget build(BuildContext context) => Container(
+ height: MediaQuery.of(context).size.height,
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ Theme.of(context).colorScheme.surface,
+ Theme.of(context).colorScheme.surfaceDim,
+ ],
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ ),
),
- ),
- child: Stack(
- children: [
- CustomScrollView(
- physics: BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
- slivers: [
- SliverPadding(
- padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
- sliver: CupertinoSliverRefreshControl(
- refreshTriggerPullDistance: 160,
- refreshIndicatorExtent: 90,
- onRefresh: () {
- unawaited(widget.nftViewModel.getNFTAssetByWallet());
+ child: Stack(
+ children: [
+ CustomScrollView(
+ physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
+ slivers: [
+ SliverPadding(
+ padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
+ sliver: CupertinoSliverRefreshControl(
+ refreshTriggerPullDistance: 160,
+ refreshIndicatorExtent: 90,
+ onRefresh: () {
+ unawaited(widget.nftViewModel.getNFTAssetByWallet());
- return widget.dashboardViewModel.refreshDashboard();
- },
+ return widget.dashboardViewModel.refreshDashboard();
+ },
+ ),
),
- ),
- SliverToBoxAdapter(
- child: Column(
- mainAxisSize: MainAxisSize.max,
- mainAxisAlignment: MainAxisAlignment.start,
- spacing: 24.0,
- children: [
- TopBar(
- key: ValueKey(widget.dashboardViewModel.wallet.id),
- dashboardViewModel: widget.dashboardViewModel,
- lightningMode: _lightningMode,
- onLightningSwitchPress: () {
- setState(() {
- _lightningMode = !_lightningMode;
- });
- },
- onSettingsButtonPress: () async {
- await CupertinoScaffold.showCupertinoModalBottomSheet(
- context: context,
- barrierColor: Colors.black.withAlpha(85),
- builder: (context) => FractionallySizedBox(
- child: Material(
- child: NewSettingsPage(
+ SliverToBoxAdapter(
+ child: Observer(
+ builder: (_) {
+ final _lightningMode = widget.dashboardViewModel.lightningMode;
+ return Column(
+ mainAxisSize: MainAxisSize.max,
+ mainAxisAlignment: MainAxisAlignment.start,
+ spacing: 24.0,
+ children: [
+ TopBar(
+ key: ValueKey(widget.dashboardViewModel.wallet.id),
dashboardViewModel: widget.dashboardViewModel,
- authService: getIt.get<AuthService>(),
- ))),
- );
-
- if (!mounted) {
- return;
- }
- _setAccountViewModel();
- await widget.dashboardViewModel.loadCardDesigns();
- if (mounted) {
- setState(() {});
- }
- },
- ),
- Column(
- spacing: 20,
- children: [
- Column(
- children: [
- Observer(
- builder: (_) => CardsView(
- key: ValueKey(widget.dashboardViewModel.wallet.name),
- onCustomizeTapped: openCardCustomizer,
- dashboardViewModel: widget.dashboardViewModel,
- accountListViewModel: accountListViewModel,
- onCompactModeBackgroundCardsTapped: openAccountCustomizer,
- lightningMode: _lightningMode,
- ),
- ),
- Observer(builder: (_) {
- return AnimatedSize(
- duration: Duration(milliseconds: 150),
- curve: Curves.easeInOutCubic,
- child: (widget.dashboardViewModel.shouldShowBalanceHiddenMessage)
- ? Column(
- children: [
- SizedBox(
- height: 12,
- width: double.infinity,
- ),
- Text(
- S.of(context).long_press_show_balance,
- style: TextStyle(
- color: Theme.of(context)
- .colorScheme
- .onSurfaceVariant),
- )
- ],
- )
- : SizedBox(width: double.infinity),
- );
- }),
- UnconfirmedBalanceWidget(
- dashboardViewModel: widget.dashboardViewModel,
- ),
- ],
- ),
- Observer(
- builder: (_) {
- return Column(
- children: [
- CoinActionRow(
- lightningMode: _lightningMode,
- showSwap: widget.dashboardViewModel.isEnabledSwapAction,
- walletType: widget.dashboardViewModel.wallet.type,
+ onLightningSwitchPress: () {
+ widget.dashboardViewModel.toggleLightningMode();
+ },
+ onSettingsButtonPress: () {
+ CupertinoScaffold.showCupertinoModalBottomSheet(
+ context: context,
+ barrierColor: Colors.black.withAlpha(85),
+ builder: (context) => FractionallySizedBox(
+ child: Material(
+ child: NewSettingsPage(
+ dashboardViewModel: widget.dashboardViewModel,
+ authService: getIt.get<AuthService>(),
+ ),
+ ),
),
- MwebAd(
- dashboardViewModel: widget.dashboardViewModel,
+ ).then((_) async {
+ widget.dashboardViewModel.accountListViewModel?.reload();
+ await Future<void>.delayed(Duration.zero);
+ await widget.dashboardViewModel.loadCardDesigns();
+ if (mounted) setState(() {});
+ });
+ },
+ ),
+ Column(
+ children: [
+ AnimatedSwitcher(
+ duration: const Duration(milliseconds: 200),
+ switchInCurve: Curves.easeOut,
+ switchOutCurve: Curves.easeOut,
+ layoutBuilder: (currentChild, previousChildren) => Stack(
+ // the front (selected) card sits at the bottom of the stack box
+ alignment: Alignment.bottomCenter,
+ children: <Widget>[
+ ...previousChildren,
+ if (currentChild != null) currentChild,
+ ],
),
- SeedBackupReminderCard(
- dashboardViewModel: widget.dashboardViewModel,
- onTap: openSeedBackupReminder,
+ child: KeyedSubtree(
+ key: ValueKey(_lightningMode),
+ child: CardsView(
+ key: ValueKey(
+ "${widget.dashboardViewModel.wallet.name}_${_lightningMode}_${widget.dashboardViewModel.accountListViewModel?.accounts.length ?? 0}_${widget.dashboardViewModel.cardDesigns.length}"),
+ onCustomizeTapped: openCardCustomizer,
+ dashboardViewModel: widget.dashboardViewModel,
+ accountListViewModel:
+ widget.dashboardViewModel.accountListViewModel,
+ onCompactModeBackgroundCardsTapped: openAccountCustomizer,
+ lightningMode: _lightningMode),
),
- ],
- );
- },
- ),
- ],
- ),
- ],
+ ),
+ Observer(
+ builder: (_) => AnimatedSize(
+ duration: const Duration(milliseconds: 150),
+ curve: Curves.easeInOutCubic,
+ child: (widget.dashboardViewModel
+ .shouldShowBalanceHiddenMessage)
+ ? Column(
+ children: [
+ const SizedBox(
+ height: 12,
+ width: double.infinity,
+ ),
+ Text(
+ S.of(context).long_press_show_balance,
+ style: TextStyle(
+ color: Theme.of(context)
+ .colorScheme
+ .onSurfaceVariant),
+ )
+ ],
+ )
+ : const SizedBox(width: double.infinity),
+ )),
+ UnconfirmedBalanceWidget(
+ dashboardViewModel: widget.dashboardViewModel),
+ SeedBackupReminderCard(
+ dashboardViewModel: widget.dashboardViewModel,
+ onTap: openSeedBackupReminder,
+ ),
+ ],
+ ),
+ Column(
+ children: [
+ CoinActionRow(
+ lightningMode: _lightningMode,
+ showSwap: widget.dashboardViewModel.isEnabledSwapAction,
+ walletType: widget.dashboardViewModel.wallet.type,
+ ),
+ MwebAd(
+ dashboardViewModel: widget.dashboardViewModel,
+ ),
+ ],
+ )
+ ],
+ );
+ },
+ ),
),
- ),
- Observer(
- builder: (_) => AssetsHistorySection(
- nftViewModel: widget.nftViewModel,
- dashboardViewModel: widget.dashboardViewModel,
+ Observer(
+ builder: (_) => AssetsHistorySection(
+ nftViewModel: widget.nftViewModel,
+ dashboardViewModel: widget.dashboardViewModel,
+ ),
),
+ const SliverToBoxAdapter(
+ child: SizedBox(height: 80.0),
+ )
+ ]),
+ Container(
+ height: (MediaQuery.of(context).padding.top),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.bottomCenter,
+ end: Alignment.topCenter,
+ colors: <Color>[
+ Theme.of(context).colorScheme.surface.withAlpha(5),
+ Theme.of(context).colorScheme.surface.withAlpha(25),
+ Theme.of(context).colorScheme.surface.withAlpha(50),
+ Theme.of(context).colorScheme.surface.withAlpha(100),
+ Theme.of(context).colorScheme.surface.withAlpha(150),
+ Theme.of(context).colorScheme.surface.withAlpha(175),
+ Theme.of(context).colorScheme.surface.withAlpha(200),
+ ],
),
- SliverToBoxAdapter(
- child: SizedBox(height: 80.0),
- )
- ]),
- Container(
- height: (MediaQuery.of(context).padding.top),
- decoration: BoxDecoration(
- gradient: LinearGradient(
- begin: Alignment.bottomCenter,
- end: Alignment.topCenter,
- colors: <Color>[
- Theme.of(context).colorScheme.surface.withAlpha(5),
- Theme.of(context).colorScheme.surface.withAlpha(25),
- Theme.of(context).colorScheme.surface.withAlpha(50),
- Theme.of(context).colorScheme.surface.withAlpha(100),
- Theme.of(context).colorScheme.surface.withAlpha(150),
- Theme.of(context).colorScheme.surface.withAlpha(175),
- Theme.of(context).colorScheme.surface.withAlpha(200),
- ],
),
),
- ),
- ],
- ),
+ ],
+ ),
+ );
+
+ Future<void> openCardCustomizer() async {
+ await CardCustomizer.show(
+ context: context,
+ dashboardViewModel: widget.dashboardViewModel,
+ lightningMode: widget.dashboardViewModel.lightningMode,
);
}
@@ -278,54 +283,19 @@ class _NewHomePageState extends State<NewHomePage> with RouteAware {
}
Future<void> openAccountCustomizer() async {
- final accountList = accountListViewModel;
- if (accountList == null) {
- return;
- }
+ if (widget.dashboardViewModel.accountListViewModel == null) return;
await CupertinoScaffold.showCupertinoModalBottomSheet(
barrierColor: Colors.black.withAlpha(60),
context: context,
- builder: (context) {
- return ModalNavigator(
- parentContext: context,
- heightMode: ModalHeightModes.fullScreen,
- rootPage: Material(
- child: AccountCustomizer(
- accountListViewModel: accountList,
- dashboardViewModel: widget.dashboardViewModel,
- ),
- ),
- );
- },
+ builder: (context) => ModalNavigator(
+ parentContext: context,
+ heightMode: ModalHeightModes.fullScreen,
+ rootPage: Material(
+ child: getIt.get<WalletAccountsPage>(param1: widget.dashboardViewModel),
+ ),
+ ),
);
await widget.dashboardViewModel.loadCardDesigns();
}
-
- void openCardCustomizer() async {
- final bloc = getIt.get<CardCustomizerBloc>(
- param1: _lightningMode,
- param2: widget.dashboardViewModel.settingsStore.displayAmountsInSatoshi);
- await CupertinoScaffold.showCupertinoModalBottomSheet(
- barrierColor: Colors.black.withAlpha(60),
- context: context,
- builder: (context) {
- return ModalNavigator(
- parentContext: context,
- heightMode: ModalHeightModes.fullScreen,
- rootPage: BlocProvider(
- create: (context) => bloc,
- child: Material(
- child: CardCustomizer(
- cryptoTitle: widget.dashboardViewModel.wallet.currency.fullName ??
- widget.dashboardViewModel.wallet.currency.name,
- cryptoName: widget.dashboardViewModel.wallet.currency.name,
- )),
- ));
- },
- );
- bloc.add(DesignSaved());
- await bloc.stream.firstWhere((s) => s is CardCustomizerSaved);
- widget.dashboardViewModel.loadCardDesigns();
- }
}
### lib/new-ui/pages/settings_page.dart
@@ -25,8 +25,6 @@ bool _hasMweb(DashboardViewModel vm) => vm.hasMweb;
bool _hasWalletConnect(DashboardViewModel vm) => vm.hasWalletConnect;
-bool _hasAccounts(DashboardViewModel vm) => vm.balanceViewModel.hasAccounts;
-
bool _requiresKeyImageSync(DashboardViewModel vm) =>
vm.wallet.type == WalletType.monero &&
[HardwareWalletType.cupcake, HardwareWalletType.trezor].contains(vm.wallet.hardwareWalletType);
@@ -65,8 +63,8 @@ class SettingsSectionData {
SettingsListItem(
"assets/new-ui/settings_row_icons/accounts.svg",
S.current.accounts,
- Routes.accountCustomizer,
- condition: _hasAccounts,
+ Routes.walletAccountsPage,
+ condition: (vm) => vm.wallet.hasAccountsSupport,
routeArgsBuilder: (vm) => vm,
),
SettingsListItem(
### lib/new-ui/pages/wallet_accounts_page.dart
@@ -0,0 +1,567 @@
+import "dart:async";
+import "dart:ui";
+
+import "package:cake_wallet/di.dart";
+import "package:cake_wallet/entities/bitcoin_amount_display_mode.dart";
+import "package:cake_wallet/entities/new_ui_entities/list_item/list_item_toggle.dart";
+import "package:cake_wallet/generated/i18n.dart";
+import "package:cake_wallet/new-ui/pages/card_customizer.dart";
+import "package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart";
+import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
+import "package:cake_wallet/src/screens/settings/widgets/account_creation_modal.dart";
+import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
+import "package:cake_wallet/src/widgets/alert_with_two_actions.dart";
+import "package:cake_wallet/src/widgets/new_list_row/new_list_section.dart";
+import "package:cake_wallet/utils/show_pop_up.dart";
+import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
+import "package:cake_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart";
+import "package:cake_wallet/view_model/wallet_account_list/account_list_item.dart";
+import "package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart";
+import "package:cw_core/balance_card_style_settings.dart";
+import "package:cw_core/card_design.dart";
+import "package:cw_core/sync_status.dart";
+import "package:cw_core/utils/print_verbose.dart";
+import "package:cw_core/wallet_type.dart";
+import "package:flutter/material.dart";
+import "package:mobx/mobx.dart";
+import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
+
+class AccountCustomizerListItem {
+ AccountCustomizerListItem({
+ required this.card,
+ required this.order,
+ required this.accountListItem,
+ });
+
+ final BalanceCard card;
+ final int order;
+ final AccountListItem accountListItem;
+}
+
+class WalletAccountsPage extends StatefulWidget {
+ const WalletAccountsPage({
+ required this.accountListViewModel,
+ required this.accountEditOrCreateViewModel,
+ required this.dashboardViewModel,
+ super.key,
+ });
+
+ final WalletAccountListViewModel accountListViewModel;
+ final WalletAccountEditOrCreateViewModel accountEditOrCreateViewModel;
+ final DashboardViewModel dashboardViewModel;
+
+ @override
+ State<WalletAccountsPage> createState() => _WalletAccountsPageState();
+}
+
+class _WalletAccountsPageState extends State<WalletAccountsPage> {
+ static const double _kStackVisibleFactor = 0.2;
+ late final double cardWidth = MediaQuery.of(context).size.width * 0.9;
+
+ final List<AccountCustomizerListItem> _items = [];
+
+ ReactionDisposer? _accountsReaction;
+
+ WalletAccountListViewModel get accountListViewModel =>
+ widget.dashboardViewModel.accountListViewModel ?? widget.accountListViewModel;
+
+ bool get _isMultiAccountsEnabled =>
+ widget.dashboardViewModel.isMultiAccountsEnabled;
+
+ String get _assetName => widget.dashboardViewModel.appStore.amountParsingProxy
+ .getCryptoSymbol(accountListViewModel.currency);
+
+
+ bool get _capitalizeAssetName =>
+ widget.dashboardViewModel.wallet.type != WalletType.bitcoin ||
+ widget.dashboardViewModel.settingsStore.displayAmountsInSatoshi !=
+ BitcoinAmountDisplayMode.satoshi;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _accountsReaction = reaction(
+ (_) => accountListViewModel.accounts
+ .map((account) => "${account.id}:${account.label}:${account.balance}")
+ .join(","),
+ (_) {
+ if (!mounted) return;
+ loadCards();
+ },
+ );
+
+ WidgetsBinding.instance.addPostFrameCallback((_) async {
+ await widget.dashboardViewModel.loadCardDesigns();
+ if (!mounted) return;
+ loadCards();
+
+ if (!_isMultiAccountsEnabled) return;
+
+ final activeId = accountListViewModel.selectedAccount?.id;
+
+ for (int i = 0; i < _items.length - 1; i++) {
+ if (activeId != null && _items[i].accountListItem.id == activeId) {
+ final lastIndex = _items.length - 1;
+ final temp = _items[i];
+ _items[i] = _items[lastIndex];
+ _items[lastIndex] = temp;
+
+ await saveCardOrder();
+ await widget.dashboardViewModel.loadCardDesigns();
+ loadCards();
+ break;
+ }
+ }
+ });
+ }
+
+ @override
+ void dispose() {
+ _accountsReaction?.call();
+ accountListViewModel.reload();
+ saveCardOrder().then((value) => widget.dashboardViewModel.loadCardDesigns());
+ super.dispose();
+ }
+
+ void loadCards() {
+ final allAccounts = accountListViewModel.accounts;
+ final visible = widget.dashboardViewModel.visibleAccounts;
+
+ if (visible.isEmpty && _items.isNotEmpty) return;
+
+ _items.clear();
+
+ if (!_isMultiAccountsEnabled) {
+ if (visible.isNotEmpty) {
+ final account = visible.first;
+ final realIndex = allAccounts.indexWhere((a) => a.id == account.id);
+ final design = (realIndex >= 0 &&
+ realIndex < widget.dashboardViewModel.cardDesigns.length)
+ ? widget.dashboardViewModel.cardDesigns[realIndex]
+ : CardDesign.genericDefault;
+
+ _items.add(AccountCustomizerListItem(
+ card: BalanceCard(
+ accountName: account.label,
+ accountIndex: account.id,
+ balance: account.balance ?? "0.00",
+ accountBalance: account.balance ?? "0.00",
+ designSwitchDuration: Duration.zero,
+ assetName: _assetName,
+ capitalizeAssetName: _capitalizeAssetName,
+ selected: true,
+ width: cardWidth,
+ design: design,
+ ),
+ order: realIndex >= 0 ? realIndex : 0,
+ accountListItem: account,
+ ));
+ }
+ if (mounted) setState(() {});
+ return;
+ }
+
+ for (int i = 0; i < allAccounts.length; i++) {
+ final realIndex = widget.dashboardViewModel.cardOrder[i];
+
+ if (realIndex == null || realIndex < 0 || realIndex >= allAccounts.length) {
+ // db order broken.
+ reset();
+ break;
+ }
+
+ _items.add(AccountCustomizerListItem(
+ card: BalanceCard(
+ accountName: allAccounts[realIndex].label,
+ accountIndex: allAccounts[realIndex].id,
+ balance: allAccounts[realIndex].balance ?? "0.00",
+ accountBalance: allAccounts[realIndex].balance ?? "0.00",
+ designSwitchDuration: Duration.zero,
+ assetName: _assetName,
+ capitalizeAssetName: _capitalizeAssetName,
+ selected: i == allAccounts.length - 1,
+ width: cardWidth,
+ design: widget.dashboardViewModel.cardDesigns[realIndex],
+ ),
+ order: realIndex,
+ accountListItem: allAccounts[realIndex]));
+ }
+ if (mounted) {
+ setState(() {});
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final showResetButton = _isMultiAccountsEnabled && _items.length > 1;
+ final showAccountsToggle = widget.dashboardViewModel.canToggleMultiAccounts;
+ final isToggleEnabled = widget.dashboardViewModel.isMultiAccountsEnabled;
+
+ if (_items.isEmpty && !showAccountsToggle) return const SizedBox.shrink();
+
+ return Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.surface,
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(24))),
+ child: Column(
+ children: [
+ ModalTopBar(
+ title: S.of(context).wallet_accounts,
+ leadingIcon: const Icon(Icons.close),
+ leadingSemanticLabel: S.of(context).close,
+ onLeadingPressed: Navigator.of(context).maybePop,
+ trailingIcon: showResetButton ? const Icon(Icons.refresh) : null,
+ trailingSemanticLabel: S.of(context).reset,
+ onTrailingPressed: showResetDialog,
+ ),
+ const SizedBox(height: 24),
+ if (showAccountsToggle)
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24),
+ child: NewListSections(
+ sections: {
+ "": [
+ ListItemToggle(
+ keyValue: S.of(context).multiple_accounts,
+ label: S.of(context).multiple_accounts,
+ value: isToggleEnabled,
+ onChanged: (val) async {
+ await widget.dashboardViewModel.setMultiAccountsEnabled(val);
+ if (!mounted) return;
+ loadCards();
+ }),
+ ],
+ },
+ ),
+ ),
+ if (_isMultiAccountsEnabled && _items.isNotEmpty)
+ Expanded(
+ child: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 24.0),
+ child: Text(
+ S.of(context).account_customizer_desc,
+ style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
+ ),
+ ),
+ Expanded(
+ child: Stack(
+ children: [
+ ReorderableListView.builder(
+ padding: const EdgeInsets.only(bottom: 196),
+ scrollController: ModalScrollController.of(context),
+ onReorder: reorder,
+ proxyDecorator: (child, index, animation) => AnimatedBuilder(
+ animation: animation,
+ builder: (context, _) {
+ final animValue = Curves.easeOutCubic.transform(animation.value);
+ final scale = lerpDouble(1, 1.05, animValue)!;
+
+ return Opacity(
+ opacity: 1 - animValue.clamp(0.0, 0.1),
+ child: Center(
+ child: SizedBox(
+ width: cardWidth,
+ child: Transform.scale(
+ scale: scale,
+ child: child,
+ ),
+ ),
+ ),
+ );
+ },
+ child: _items[index].card,
+ ),
+ itemCount: _items.length,
+ itemBuilder: (context, int index) {
+ final card = _items[index].card;
+ final selectedItemIndex = _items.length - 1;
+
+ return Container(
+ key: ValueKey(index),
+ child: Semantics(
+ button: true,
+ selected: selectedItemIndex == index,
+ label: _items[index].accountListItem.label,
+ onTap: () => reorder(index, _items.length),
+ child: GestureDetector(
+ excludeFromSemantics: true,
+ onTap: () => reorder(index, _items.length),
+ child: Align(
+ alignment: Alignment.topCenter,
+ heightFactor: _kStackVisibleFactor,
+ child: card,
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.only(bottom: 50),
+ child: Align(
+ alignment: Alignment.bottomCenter,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24),
+ child: Material(
+ color: Colors.transparent,
+ child: MergeSemantics(
+ child: Semantics(
+ button: true,
+ child: InkWell(
+ borderRadius: BorderRadius.circular(999999),
+ onTap: _showAddAccountModal,
+ child: Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.surfaceContainer,
+ borderRadius: BorderRadius.circular(999999),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 18),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ spacing: 8,
+ children: [
+ Icon(
+ Icons.add,
+ size: 28,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ Text(
+ S.of(context).add_account,
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.primary,
+ fontWeight: FontWeight.w500,
+ ),
+ )
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ bool _checkReadyToManage() {
+ if (widget.dashboardViewModel.wallet.type == WalletType.bitcoin) {
+ return true;
+ }
+ if (widget.dashboardViewModel.status is! SyncedSyncStatus) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertWithOneAction(
+ alertTitle: S.of(context).wallet_is_syncing,
+ alertContent: S.of(context).cannot_manage_accounts_during_sync,
+ buttonText: S.of(context).ok,
+ buttonAction: Navigator.of(context).pop,
+ ),
+ );
+ return false;
+ }
+ return true;
+ }
+
+ Future<void> _showAddAccountModal() async {
+ if (!_checkReadyToManage()) return;
+
+ final createViewModel = getIt.get<WalletAccountEditOrCreateViewModel>();
+
+ final res = await showCupertinoModalBottomSheet(
+ context: context,
+ backgroundColor: Colors.transparent,
+ builder: (context) => Material(
+ child: AccountCreationModal(
+ viewModel: createViewModel,
+ ),
+ ));
+
+ if (res != null && res is bool && res == true) {
+ await accountListViewModel.reload();
+ await Future<void>.delayed(Duration.zero);
+
+ final accounts = accountListViewModel.accounts;
+ if (accounts.isNotEmpty) {
+ final newestAccount = accounts.reduce((a, b) => a.id > b.id ? a : b);
+ await accountListViewModel.select(newestAccount);
+ }
+
+ await widget.dashboardViewModel.loadCardDesigns();
+ loadCards();
+ if (mounted) setState(() {});
+ }
+ }
+
+ Future<void> _openCardCustomizer() async {
+ if (!_checkReadyToManage()) {
+ return;
+ }
+
+ await CardCustomizer.show(
+ context: context,
+ dashboardViewModel: widget.dashboardViewModel,
+ lightningMode: widget.dashboardViewModel.lightningMode,
+ asModalSheet: false,
+ onSaved: _reloadAccountsAndCards,
+ );
+ }
+
+ Future<void> _reloadAccountsAndCards() async {
+ await accountListViewModel.reload();
+ await widget.dashboardViewModel.loadCardDesigns();
+ if (!mounted) return;
+ loadCards();
+ }
+
+ void _onCardsReordered(int oldRealIndex, int newRealIndex) {
+ final accounts = accountListViewModel.accounts;
+ if (oldRealIndex < 0 ||
+ newRealIndex < 0 ||
+ oldRealIndex >= accounts.length ||
+ newRealIndex >= accounts.length) {
+ return;
+ }
+
+ final oldAccountId = accounts[oldRealIndex].id;
+ final newAccountId = accounts[newRealIndex].id;
+
+ final from = _items.indexWhere((item) => item.accountListItem.id == oldAccountId);
+ final to = _items.indexWhere((item) => item.accountListItem.id == newAccountId);
+ if (from < 0 || to < 0 || from == to) return;
+
+ final insertIndex = from < to ? to + 1 : to;
+ reorder(from, insertIndex);
+
+ saveCardOrder().then((_) {
+ if (!mounted) return;
+ widget.dashboardViewModel.loadCardDesigns();
+ });
+ }
+
+ void reorder(int oldIndex, int newIndex) {
+ if (!_isMultiAccountsEnabled) return;
+ setState(() {
+ if (oldIndex < newIndex) {
+ newIndex -= 1;
+ }
+ final AccountCustomizerListItem item = _items.removeAt(oldIndex);
+ _items.insert(newIndex, item);
+ });
+
+ // necessary to copy all this to keep constant constructor for BalanceCard
+ for (int i = 0; i < _items.length; i++) {
+ _items[i] = AccountCustomizerListItem(
+ card: BalanceCard(
+ accountName: _items[i].card.accountName,
+ balance: _items[i].card.balance,
+ accountIndex: _items[i].card.accountIndex,
+ accountBalance: _items[i].card.accountBalance,
+ assetName: _items[i].card.assetName,
+ capitalizeAssetName: _items[i].card.capitalizeAssetName,
+ designSwitchDuration: _items[i].card.designSwitchDuration,
+ onCustomizeTapped: (i == _items.length - 1) ? _openCardCustomizer : null,
+ selected: i == _items.length - 1,
+ width: _items[i].card.width,
+ design: _items[i].card.design,
+ ),
+ order: i,
+ accountListItem: _items[i].accountListItem);
+ }
+
+ if (newIndex == _items.length - 1 || oldIndex == _items.length - 1) {
+ accountListViewModel.select(_items[_items.length - 1].accountListItem);
+ }
+ }
+
+ Future<void> saveCardOrder() async {
+
+ if (!_isMultiAccountsEnabled) return;
+
+ for (int orderIndex = 0; orderIndex < _items.length; orderIndex++) {
+ final item = _items[orderIndex];
+ printV("${item.accountListItem.id}: $orderIndex");
+
+ final existing = await BalanceCardStyleSettings.get(
+ widget.dashboardViewModel.wallet.walletInfo.internalId,
+ item.accountListItem.id,
+ );
+
+ await BalanceCardStyleSettings.fromCardDesign(
+ walletInfoId: widget.dashboardViewModel.wallet.walletInfo.internalId,
+ accountIndex: item.accountListItem.id,
+ cardOrder: orderIndex,
+ design: item.card.design,
+ iconStyleIndex: existing?.iconStyleIndex ?? 0,
+ gradientIndexOverride: existing?.gradientIndex)
+ .insert();
+ }
+ }
+
+ Future<void> showResetDialog() async {
+ final res = await showPopUp(
+ context: context,
+ builder: (context) => AlertWithTwoActions(
+ alertTitle: S.of(context).reset,
+ alertContent: S.of(context).card_order_reset_desc,
+ leftButtonText: S.of(context).yes,
+ rightButtonText: S.of(context).no,
+ actionLeftButton: () {
+ Navigator.of(context).pop(true);
+ },
+ actionRightButton: Navigator.of(context).pop));
+ if (res != null && res is bool && res) {
+ reset();
+ }
+ }
+
+ Future<void> reset() async {
+ _items.clear();
+
+ final accounts = accountListViewModel.accounts;
+ for (int i = 0; i < accounts.length; i++) {
+ _items.add(AccountCustomizerListItem(
+ card: BalanceCard(
+ accountName: accounts[i].label,
+ accountIndex: accounts[i].id,
+ balance: accounts[i].balance ?? "0.00",
+ accountBalance: accounts[i].balance ?? "0.00",
+ assetName: _assetName,
+ capitalizeAssetName: _capitalizeAssetName,
+ selected: i == accounts.length - 1,
+ designSwitchDuration: const Duration(milliseconds: 200),
+ width: cardWidth,
+ design: i >= widget.dashboardViewModel.cardDesigns.length
+ ? CardDesign.genericDefault
+ : widget.dashboardViewModel.cardDesigns[i],
+ ),
+ order: i,
+ accountListItem: accounts[i]));
+ }
+
+ await saveCardOrder();
+
+ if (accounts.isNotEmpty) {
+ await accountListViewModel.select(accounts.last);
+ }
+
+ await widget.dashboardViewModel.loadCardDesigns();
+ if (mounted) setState(() {});
+ }
+}
### lib/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart
@@ -1,4 +1,5 @@
import 'package:bloc/bloc.dart';
+import "package:cake_wallet/bitcoin/bitcoin.dart";
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/wownero/wownero.dart';
import "package:cw_core/balance_card_style_settings.dart";
@@ -30,6 +31,9 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
add(_Init());
}
+ bool get _accountNameEnabled =>
+ !lightningMode && _wallet.walletInfo.isMultiAccountsEnabled == true;
+
List<Gradient> _updateAvailableColors(CardDesign currentDesign) {
final list = List<Gradient>.from(CardDesign.allGradients, growable: true);
if (CardDesign.specialDesignsForCurrencies[_wallet.currency] != null) {
@@ -83,23 +87,9 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
}
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;
- }
+ 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, curr);
@@ -123,6 +113,39 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
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 = 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;
@@ -152,39 +175,44 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
emit(state.copyWith(accountName: event.newAccountName));
}
- void _onDesignSaved(DesignSaved event, Emitter<CardCustomizerState> emit) {
- BalanceCardStyleSettings.fromCardDesign(
+ Future<void> _onDesignSaved(DesignSaved 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)
- .insert()
- .then((value) {
- emit(CardCustomizerSaved(
- state.selectedDesignIndex,
- state.selectedColorIndex,
- state.availableDesigns,
- state.availableColors,
- state.accountName,
- state.accountIndex,
- state.displaySats,
- state.cardOrder,
- availableIconPaths: state.availableIconPaths,
- selectedIconIndex: state.selectedIconIndex));
- });
- saveAccountName();
+ .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> saveAccountName() async {
+ if (!_accountNameEnabled) return;
+
if (_wallet.type == WalletType.monero) {
await saveMoneroAccountName();
}
if (_wallet.type == WalletType.wownero) {
await saveWowneroAccountName();
}
+ if (_wallet.type == WalletType.bitcoin && !lightningMode) {
+ await saveBitcoinAccountName();
+ }
}
Future<void> saveMoneroAccountName() async {
@@ -202,4 +230,11 @@ class CardCustomizerBloc extends Bloc<CardCustomizerEvent, CardCustomizerState>
await _wallet.save();
}
+
+ Future<void> saveBitcoinAccountName() async {
+ await _wallet.walletInfo.renameAccount(
+ accountIndex: state.accountIndex,
+ label: state.accountName,
+ );
+ }
}
### lib/new-ui/widgets/coins_page/cards/balance_card.dart
@@ -215,10 +215,12 @@ class BalanceCard extends StatelessWidget {
child: AnimatedSwitcher(
duration: designSwitchDuration,
layoutBuilder: (currentChild, previousChildren) {
+ final currentKey = currentChild?.key;
return Stack(
alignment: Alignment.centerLeft,
children: <Widget>[
- ...previousChildren,
+ for (final child in previousChildren)
+ if (child.key != currentKey) child,
if (currentChild != null) currentChild,
],
);
### lib/new-ui/widgets/coins_page/cards/cards_view.dart
@@ -13,7 +13,7 @@ import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
-import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
+import 'package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart';
import 'package:cw_core/card_design.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/unspent_coin_type.dart';
@@ -30,14 +30,14 @@ import 'balance_card.dart';
class CardsView extends StatefulWidget {
const CardsView(
{super.key,
- required this.dashboardViewModel,
- required this.accountListViewModel,
- required this.lightningMode,
- required this.onCompactModeBackgroundCardsTapped,
- required this.onCustomizeTapped});
+ required this.dashboardViewModel,
+ required this.accountListViewModel,
+ required this.lightningMode,
+ required this.onCompactModeBackgroundCardsTapped,
+ required this.onCustomizeTapped});
final DashboardViewModel dashboardViewModel;
- final MoneroAccountListViewModel? accountListViewModel;
+ final WalletAccountListViewModel? accountListViewModel;
final VoidCallback onCompactModeBackgroundCardsTapped;
final VoidCallback onCustomizeTapped;
final bool lightningMode;
@@ -49,22 +49,39 @@ class CardsView extends StatefulWidget {
class _CardsViewState extends State<CardsView> {
late int _selectedIndex;
bool isFirstBuild = true;
+ ReactionDisposer? _cardOrderReaction;
+
+ int get _fallbackSelectedIndex =>
+ max(0, widget.dashboardViewModel.cardOrder.length - 1);
@override
void initState() {
super.initState();
- _selectedIndex = widget.dashboardViewModel.cardOrder.length - 1;
- reaction(
- (_) => widget.dashboardViewModel.cardOrder.values.toList(),
- (_) => setState(() {
- _selectedIndex = widget.dashboardViewModel.cardOrder.length - 1;
- }));
+ _selectedIndex = _fallbackSelectedIndex;
+ _cardOrderReaction = reaction(
+ (_) => widget.dashboardViewModel.cardOrder.values.toList(),
+ (_) => setState(() {
+ _selectedIndex = _fallbackSelectedIndex;
+ }),
+ );
+ }
+
+ @override
+ void dispose() {
+ _cardOrderReaction?.call();
+ super.dispose();
}
static const Duration animDuration = Duration(milliseconds: 200);
static const int compactModeTreshold = 4;
static const int maxCards = 5;
+ /// No account name on the card: Lightning card, or Bitcoin with multi-accounts off.
+ bool get _hideAccountInfo =>
+ widget.lightningMode ||
+ (widget.dashboardViewModel.wallet.type == WalletType.bitcoin &&
+ !widget.dashboardViewModel.isMultiAccountsEnabled);
+
Widget _buildCard(int visualIndex, int realIndex, int numCards, double parentWidth,
Map<int, int> order, bool compactMode, double overlapAmount) {
final baseTop = overlapAmount * (numCards - 1);
@@ -79,7 +96,7 @@ class _CardsViewState extends State<CardsView> {
final isSelected = _selectedIndex == visualIndex;
final accounts = widget.accountListViewModel?.accounts;
- final cardLabel = (accounts != null && realIndex < accounts.length)
+ final cardLabel = (!_hideAccountInfo && accounts != null && realIndex < accounts.length)
? accounts[realIndex].label
: S.of(context).balance;
@@ -89,7 +106,7 @@ class _CardsViewState extends State<CardsView> {
widget.onCompactModeBackgroundCardsTapped();
} else if (!compactMode) {
setState(() {
- if (widget.accountListViewModel != null)
+ if (widget.accountListViewModel != null && !widget.lightningMode)
widget.accountListViewModel!.select(widget.accountListViewModel!.accounts[realIndex]);
_selectedIndex = visualIndex;
});
@@ -121,9 +138,9 @@ class _CardsViewState extends State<CardsView> {
label: cardLabel,
hint: isSelected
? (widget.dashboardViewModel.balanceViewModel.displayMode ==
- BalanceDisplayMode.hiddenBalance
- ? S.of(context).long_press_show_balance
- : S.of(context).long_press_hide_balance)
+ BalanceDisplayMode.hiddenBalance
+ ? S.of(context).long_press_show_balance
+ : S.of(context).long_press_hide_balance)
: null,
onTap: onCardTap,
onLongPress: isSelected ? onCardLongPress : null,
@@ -168,13 +185,14 @@ class _CardsViewState extends State<CardsView> {
realIndex >= widget.dashboardViewModel.cardDesigns.length)
cardDesign = CardDesign.genericDefault;
else if (widget.lightningMode)
- cardDesign = widget.dashboardViewModel.cardDesigns[realIndex + 1];
+ // the lightning design is always the last one (after all bitcoin accounts)
+ cardDesign = widget.dashboardViewModel.cardDesigns.last;
else
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex];
final String accountName;
final String accountBalance;
- if (account == null) {
+ if (account == null || _hideAccountInfo) {
accountName = "";
accountBalance = "";
} else {
@@ -188,30 +206,30 @@ class _CardsViewState extends State<CardsView> {
final List<BalanceCardAction> actions = widget.lightningMode
? [
- BalanceCardAction(
- label: S.current.bitcoin_lightning_deposit,
- icon: Icons.arrow_downward,
- onTap: depositToL2,
- ),
- BalanceCardAction(
- label: S.current.bitcoin_lightning_withdraw,
- icon: Icons.arrow_upward,
- onTap: withdrawFromL2,
- )
- ]
+ BalanceCardAction(
+ label: S.current.bitcoin_lightning_deposit,
+ icon: Icons.arrow_downward,
+ onTap: depositToL2,
+ ),
+ BalanceCardAction(
+ label: S.current.bitcoin_lightning_withdraw,
+ icon: Icons.arrow_upward,
+ onTap: withdrawFromL2,
+ )
+ ]
: widget.dashboardViewModel.isEnabledTradeAction
- ? [
- BalanceCardAction(
- label: S.current.buy,
- icon: Icons.arrow_forward_ios_rounded,
- iconSize: 12,
- onTap: () {
- showModalBottomSheet(
- context: context, builder: (context) => BuySellSelectorModal());
- },
- )
- ]
- : [];
+ ? [
+ BalanceCardAction(
+ label: S.current.buy,
+ icon: Icons.arrow_forward_ios_rounded,
+ iconSize: 12,
+ onTap: () {
+ showModalBottomSheet(
+ context: context, builder: (context) => BuySellSelectorModal());
+ },
+ )
+ ]
+ : [];
return BalanceCard(
width: effectiveCardWidth,
@@ -263,31 +281,55 @@ class _CardsViewState extends State<CardsView> {
double _getBoxHeight(int numCards, double overlapAmount) {
return
- /* height of initial card */
- (2 / 3.2) * (effectiveCardWidth) +
- /* height of bg card * amount of bg cards */
- overlapAmount * ((numCards) - 1);
+ /* height of initial card */
+ (2 / 3.2) * (effectiveCardWidth) +
+ /* height of bg card * amount of bg cards */
+ overlapAmount * ((numCards) - 1);
+ }
+
+
+ int _visibleCardsCount() {
+ if (widget.dashboardViewModel.wallet.type != WalletType.bitcoin) {
+ return widget.dashboardViewModel.cardDesigns.length;
+ }
+
+ if (widget.lightningMode || !widget.dashboardViewModel.isMultiAccountsEnabled) {
+ return 1;
+ }
+
+ return widget.accountListViewModel?.accounts.length ?? 1;
+ }
+
+ int? _visualIndexForSelectedAccount(Map<int, int> order) {
+ final vm = widget.accountListViewModel;
+ if (vm == null || _hideAccountInfo) return null;
+
+ final selectedId = vm.selectedAccount?.id;
+ final realIndex = vm.accounts.indexWhere((a) => a.id == selectedId);
+ if (realIndex < 0) return null;
+
+ for (final entry in order.entries) {
+ if (entry.value == realIndex) return entry.key;
+ }
+ return null;
}
@override
- Widget build(BuildContext context) {
- return Observer(builder: (context) {
+ Widget build(BuildContext context) => Observer(builder: (context) {
final parentWidth = MediaQuery.of(context).size.width;
final children = <Widget>[];
- int numCards = widget.dashboardViewModel.wallet.type == WalletType.bitcoin
- ? 1
- : widget.dashboardViewModel.cardDesigns.length;
+ int numCards = _visibleCardsCount();
if (numCards == 0) numCards = 1;
- if (_selectedIndex >= (numCards)) {
+ if (_selectedIndex >= numCards) {
_selectedIndex = 0;
}
Map<int, int> order = widget.dashboardViewModel.cardOrder.length != numCards
? Map<int, int>.fromEntries(
- List.generate(numCards, (i) => MapEntry(i, i)),
- )
+ List.generate(numCards, (i) => MapEntry(i, i)),
+ )
: widget.dashboardViewModel.cardOrder;
for (int i = min(numCards - 1, maxCards); i >= 0; i--) {
@@ -299,21 +341,16 @@ class _CardsViewState extends State<CardsView> {
}
}
+ final followIndex = _visualIndexForSelectedAccount(order);
+ if (followIndex != null) _selectedIndex = followIndex;
+
final bool compactMode = numCards >= compactModeTreshold;
final double overlapAmount = compactMode ? 5.0 : 46.0;
for (int i = min(numCards - 1, maxCards); i >= 0; i--) {
int visualIndex = (_selectedIndex - i + numCards) % numCards;
int realIndex = order[visualIndex]!;
- if (visualIndex == _selectedIndex &&
- widget.accountListViewModel != null &&
- realIndex < widget.accountListViewModel!.accounts.length &&
- widget.accountListViewModel?.selected.label !=
- widget.accountListViewModel?.accounts[realIndex].label) {
- widget.accountListViewModel!.select(widget.accountListViewModel!.accounts[realIndex]);
- }
-
children.add(_buildCard(
visualIndex, realIndex, numCards, parentWidth, order, compactMode, overlapAmount));
}
@@ -331,7 +368,6 @@ class _CardsViewState extends State<CardsView> {
),
);
});
- }
Future<void> depositToL2() async {
PaymentRequest? paymentRequest = null;
@@ -343,7 +379,7 @@ class _CardsViewState extends State<CardsView> {
}
} else if (widget.dashboardViewModel.type == WalletType.bitcoin) {
final depositAddress =
- await bitcoin!.getUnusedSpakDepositAddress(widget.dashboardViewModel.wallet);
+ await bitcoin!.getUnusedSpakDepositAddress(widget.dashboardViewModel.wallet);
if ((depositAddress?.isNotEmpty ?? false)) {
paymentRequest = PaymentRequest.fromUri(Uri.parse("bitcoin:$depositAddress"));
}
@@ -352,10 +388,10 @@ class _CardsViewState extends State<CardsView> {
if (widget.dashboardViewModel.type == WalletType.bitcoin) {
final page = getIt.get<NewSendPage>(
param1: SendPageParams(
- initialPaymentRequest: paymentRequest,
- unspentCoinType: UnspentCoinType.nonMweb,
- mode: SendPageModes.lightningDeposit,
- ));
+ initialPaymentRequest: paymentRequest,
+ unspentCoinType: UnspentCoinType.nonMweb,
+ mode: SendPageModes.lightningDeposit,
+ ));
showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(128),
@@ -399,10 +435,10 @@ class _CardsViewState extends State<CardsView> {
if (widget.dashboardViewModel.type == WalletType.bitcoin) {
final page = getIt.get<NewSendPage>(
param1: SendPageParams(
- initialPaymentRequest: paymentRequest,
- unspentCoinType: unspentCoinType,
- mode: SendPageModes.lightningWithdrawal,
- ));
+ initialPaymentRequest: paymentRequest,
+ unspentCoinType: unspentCoinType,
+ mode: SendPageModes.lightningWithdrawal,
+ ));
showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(128),
@@ -425,4 +461,4 @@ class _CardsViewState extends State<CardsView> {
);
}
}
-}
+}
\ No newline at end of file
### lib/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart
@@ -16,14 +16,12 @@ import 'package:mobx/mobx.dart';
class TopBar extends StatefulWidget {
const TopBar({
- required this.lightningMode,
required this.onLightningSwitchPress,
required this.dashboardViewModel,
required this.onSettingsButtonPress,
super.key,
});
- final bool lightningMode;
final VoidCallback onLightningSwitchPress;
final VoidCallback onSettingsButtonPress;
final DashboardViewModel dashboardViewModel;
@@ -73,8 +71,8 @@ class _TopBarState extends State<TopBar> {
void _bindStatusReaction() {
_statusReactionDisposer = reaction(
- (_) => widget.dashboardViewModel.status.runtimeType,
- (status) {
+ (_) => widget.dashboardViewModel.status.runtimeType,
+ (status) {
syncedMessageTimer?.cancel();
syncedMessageTimer = null;
@@ -107,78 +105,78 @@ class _TopBarState extends State<TopBar> {
@override
Widget build(BuildContext context) => Padding(
- padding: EdgeInsets.only(left: 18, right: 18, top: 10 + _additionalTopPadding(context)),
- child: Observer(
- builder: (_) => Row(
- spacing: 12,
- children: [
- widget.dashboardViewModel.hasLightning
- ? LightningSwitcher(
- lightningMode: widget.lightningMode,
- onLightningSwitchPress: widget.onLightningSwitchPress,
- )
- : ChainIcon(
- iconPath: widget.dashboardViewModel.wallet.currency.flatIconPath ?? "",
- dashboardViewModel: widget.dashboardViewModel,
- isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
- showSyncedMessage: showSyncedMessage,
- ),
- Expanded(
- child: SizedBox(
- height: 36,
- child: AnimatedSwitcher(
- duration: const Duration(milliseconds: 150),
- reverseDuration: Duration.zero,
- layoutBuilder: (currentChild, previousChildren) => Stack(
- alignment: Alignment.centerLeft,
- children: [
- ...previousChildren,
- if (currentChild != null) currentChild,
- ],
+ padding: EdgeInsets.only(
+ bottom: 10, left: 18, right: 18, top: 10 + _additionalTopPadding(context)),
+ child: Observer(
+ builder: (_) => Row(
+ spacing: 12,
+ children: [
+ widget.dashboardViewModel.hasLightning
+ ? LightningSwitcher(
+ lightningMode: widget.dashboardViewModel.lightningMode,
+ onLightningSwitchPress: widget.onLightningSwitchPress,
+ )
+ : ChainIcon(
+ iconPath: widget.dashboardViewModel.wallet.currency.flatIconPath ?? "",
+ dashboardViewModel: widget.dashboardViewModel,
+ isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
+ showSyncedMessage: showSyncedMessage,
+ ),
+ Expanded(
+ child: SizedBox(
+ height: 36,
+ child: AnimatedSwitcher(
+ duration: const Duration(milliseconds: 150),
+ reverseDuration: Duration.zero,
+ layoutBuilder: (currentChild, previousChildren) => Stack(
+ alignment: Alignment.centerLeft,
+ children: [
+ ...previousChildren,
+ if (currentChild != null) currentChild,
+ ],
+ ),
+ child: replacesWalletName
+ ? SyncBar(
+ dashboardViewModel: widget.dashboardViewModel,
+ isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
+ showSyncedMessage: showSyncedMessage,
+ )
+ : Row(
+ children: [
+ Expanded(
+ child: WalletInfoBar(
+ name: widget.dashboardViewModel.wallet.name,
+ hardwareWalletType: widget.dashboardViewModel.wallet.hardwareWalletType,
+ ),
),
- child: replacesWalletName
- ? SyncBar(
- dashboardViewModel: widget.dashboardViewModel,
- isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
- showSyncedMessage: showSyncedMessage,
- )
- : Row(
- children: [
- Expanded(
- child: WalletInfoBar(
- name: widget.dashboardViewModel.wallet.name,
- hardwareWalletType:
- widget.dashboardViewModel.wallet.hardwareWalletType,
- ),
- ),
- if (widget.dashboardViewModel.isTorEnabled) ...[
- const SizedBox(width: 6),
- SyncBar(
- dashboardViewModel: widget.dashboardViewModel,
- isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
- showSyncedMessage: showSyncedMessage,
- ),
- ],
- ],
- ),
- ),
+ if (widget.dashboardViewModel.isTorEnabled) ...[
+ const SizedBox(width: 6),
+ SyncBar(
+ dashboardViewModel: widget.dashboardViewModel,
+ isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
+ showSyncedMessage: showSyncedMessage,
+ ),
+ ],
+ ],
),
),
- ModernButton.svg(
- key: const ValueKey("home_page_settings_button_key"),
- iconColor: Theme.of(context).colorScheme.primary,
- size: 36,
- onPressed: () {
- HapticFeedback.mediumImpact();
- widget.onSettingsButtonPress();
- },
- svgPath: "assets/new-ui/top-settings.svg",
- semanticLabel: S.of(context).settings_title,
- ),
- ],
+ ),
+ ),
+ ModernButton.svg(
+ key: const ValueKey("home_page_settings_button_key"),
+ iconColor: Theme.of(context).colorScheme.primary,
+ size: 36,
+ onPressed: () {
+ HapticFeedback.mediumImpact();
+ widget.onSettingsButtonPress();
+ },
+ svgPath: "assets/new-ui/top-settings.svg",
+ semanticLabel: S.of(context).settings_title,
),
- ),
- );
+ ],
+ ),
+ ),
+ );
//FIXME remove after this gets fixed flutter-side
double _additionalTopPadding(BuildContext context) {
@@ -188,4 +186,4 @@ class _TopBarState extends State<TopBar> {
return 0;
}
-}
+}
\ No newline at end of file
### lib/new-ui/widgets/coins_page/wallet_info.dart
@@ -4,11 +4,7 @@ import 'package:cw_core/wallet_info.dart';
import 'package:flutter/material.dart';
class WalletInfoBar extends StatelessWidget {
- const WalletInfoBar({
- required this.name,
- required this.hardwareWalletType,
- super.key,
- });
+ const WalletInfoBar({required this.name, required this.hardwareWalletType, super.key});
final String name;
final HardwareWalletType? hardwareWalletType;
@@ -22,6 +18,8 @@ class WalletInfoBar extends StatelessWidget {
label: semanticsLabel,
child: ExcludeSemantics(
child: Row(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
@@ -33,7 +31,7 @@ class WalletInfoBar extends StatelessWidget {
child: hardwareWalletIcon == null
? const SizedBox.shrink(key: ValueKey("empty"))
: Padding(
- padding: const EdgeInsets.only(right: 4),
+ padding: const EdgeInsets.only(right: 8),
child: CakeImageWidget(
imageUrl: hardwareWalletIcon!,
key: const ValueKey("hardware_wallet_icon"),
### lib/router.dart
@@ -4,7 +4,6 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import "package:cake_wallet/core/auth_service.dart";
import 'package:cake_wallet/core/new_wallet_arguments.dart';
import 'package:cake_wallet/new-ui/new_dashboard.dart';
-import "package:cake_wallet/new-ui/pages/account_customizer.dart";
import 'package:cake_wallet/new-ui/pages/about_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_history_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart';
@@ -51,6 +50,7 @@ import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_cache_debug.dart';
import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
import 'package:cake_wallet/src/screens/dev/network_requests.dart';
+import 'package:cake_wallet/new-ui/pages/wallet_accounts_page.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/src/screens/dev/qr_tools_page.dart';
@@ -129,8 +129,8 @@ import "package:cake_wallet/view_model/dashboard/nft_send_view_model.dart";
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
+import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart';
-import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart';
import 'package:cake_wallet/view_model/seed_settings_view_model.dart';
@@ -617,13 +617,6 @@ Route<dynamic> createRoute(RouteSettings settings) {
param1: args?['editingNode'] as Node?, param2: args?['isSelected'] as bool?);
return CupertinoPageRoute<void>(builder: (_) => page);
- case Routes.accountCustomizer:
- return handleRouteWithPlatformAwareness(
- (_) => getIt.get<AccountCustomizer>(
- param1: settings.arguments! as DashboardViewModel,
- ),
- );
-
case Routes.addressBook:
return handleRouteWithPlatformAwareness(
(context) => getIt.get<ContactListPage>(),
@@ -853,6 +846,12 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.manageNodes:
return MaterialPageRoute<void>(builder: (_) => getIt.get<ManageNodesPage>(param1: false));
+ case Routes.walletAccountsPage:
+ final dashboardViewModel = settings.arguments as DashboardViewModel;
+ return handleRouteWithPlatformAwareness(
+ (_) => getIt.get<WalletAccountsPage>(param1: dashboardViewModel),
+ );
+
case Routes.managePowNodes:
return MaterialPageRoute<void>(builder: (_) => getIt.get<ManageNodesPage>(param1: true));
### lib/routes.dart
@@ -31,8 +31,6 @@ class Routes {
static const newPowNode = '/new_pow_node_list';
static const login = '/login';
static const splash = '/splash';
- static const accountCreation = '/account_new';
- static const accountCustomizer = "/account_customizer";
static const nanoAccountCreation = '/nano_account_new';
static const addressBook = '/address_book';
static const pickerAddressBook = '/picker_address_book';
@@ -112,6 +110,7 @@ class Routes {
static const setup2faInfoPage = '/setup_2fa_info_page';
static const homeSettings = '/home_settings';
static const editToken = '/edit_token';
+ static const walletAccountsPage = '/wallet_accounts_page';
static const manageNodes = '/manage_nodes';
static const managePowNodes = '/manage_pow_nodes';
static const walletConnectConnectionsListing = '/wallet-connect-connections-listing';
### lib/src/screens/settings/widgets/account_creation_modal.dart
@@ -0,0 +1,149 @@
+import "package:cake_wallet/core/execution_state.dart";
+import "package:cake_wallet/generated/i18n.dart";
+import "package:cake_wallet/new-ui/widgets/modal_grab_handle.dart";
+import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
+import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
+import "package:cake_wallet/src/widgets/cake_image_widget.dart";
+import "package:cake_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart";
+import "package:cw_core/generate_name.dart";
+import "package:flutter/material.dart";
+import "package:flutter/services.dart";
+import "package:flutter_mobx/flutter_mobx.dart";
+
+class AccountCreationModal extends StatefulWidget {
+ const AccountCreationModal({
+ required this.viewModel,
+ super.key,
+ });
+
+ final WalletAccountEditOrCreateViewModel viewModel;
+
+ @override
+ State<AccountCreationModal> createState() => _AccountCreationModalState();
+}
+
+class _AccountCreationModalState extends State<AccountCreationModal> {
+ static const int maxAccountNameLength = 25;
+
+ final TextEditingController _controller = TextEditingController();
+
+ @override
+ void initState() {
+ super.initState();
+ _controller.text = widget.viewModel.label;
+ _controller.addListener(() {
+ widget.viewModel.label = _controller.text;
+ });
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+
+ Future<void> _onSubmit() async {
+ if (widget.viewModel.state is IsExecutingState) return;
+ if (_controller.text.trim().isEmpty || _controller.text.length > maxAccountNameLength) return;
+
+ await widget.viewModel.save();
+
+ if (!mounted) return;
+
+ if (widget.viewModel.state is ExecutedSuccessfullyState) {
+ Navigator.of(context).pop(true);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) => Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.surface,
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(30))),
+ child: SafeArea(
+ top: false,
+ child: Padding(
+ padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const ModalGrabHandle(),
+ ModalTopBar(
+ title: widget.viewModel.isEdit
+ ? S.of(context).edit_account
+ : S.of(context).create_account,
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 18),
+ child: Column(
+ spacing: 50,
+ children: [
+ const SizedBox(),
+ Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.surfaceContainer,
+ borderRadius: BorderRadius.circular(16),
+ ),
+ child: Row(
+ children: [
+ Expanded(
+ child: TextField(
+ controller: _controller,
+ maxLength: maxAccountNameLength,
+ inputFormatters: [
+ LengthLimitingTextInputFormatter(maxAccountNameLength),
+ ],
+ decoration: InputDecoration(
+ hintText: S.of(context).account_name,
+ counterText: "",
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.all(12),
+ child: GestureDetector(
+ onTap: () async {
+ final generated = await generateName();
+ _controller.text = generated.length > maxAccountNameLength
+ ? generated.substring(0, maxAccountNameLength)
+ : generated;
+ },
+ child: Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.surfaceContainerHigh,
+ borderRadius: BorderRadius.circular(5)),
+ child: CakeImageWidget(
+ imageUrl: "assets/new-ui/randomize.svg",
+ colorFilter: ColorFilter.mode(
+ Theme.of(context).colorScheme.primary, BlendMode.srcIn),
+ ),
+ ),
+ ),
+ )
+ ],
+ ),
+ ),
+ ValueListenableBuilder<TextEditingValue>(
+ valueListenable: _controller,
+ builder: (context, value, _) => Observer(
+ builder: (_) => NewPrimaryButton(
+ onPressed: _onSubmit,
+ text: S.of(context).continue_text,
+ color: Theme.of(context).colorScheme.primary,
+ textColor: Theme.of(context).colorScheme.onPrimary,
+ disabled: value.text.trim().isEmpty ||
+ value.text.length > maxAccountNameLength,
+ isLoading: widget.viewModel.state is IsExecutingState,
+ ),
+ ),
+ ),
+ const SizedBox(),
+ ],
+ ),
+ )
+ ],
+ ),
+ ),
+ ),
+ );
+}
### lib/view_model/dashboard/balance_view_model.dart
@@ -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;
### lib/view_model/dashboard/dashboard_view_model.dart
@@ -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,15 +119,15 @@ abstract class DashboardViewModelBase with Store {
unawaited(isBackgroundSyncEnabled());
unawaited(isBatteryOptimizationEnabled());
unawaited(_loadConstraints());
+ accountListViewModel = accountListViewModelFactory();
final _wallet = wallet;
loadFilterItems();
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,29 +183,22 @@ 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;
// }
_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;
- }
- if (isRelevant) {
- relevantTxs.add(tx);
- }
+ final allTxs = appStore.wallet!.transactionHistory.transactions.values;
+ final List<TransactionInfo> relevantTxs;
+
+ 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) {
### lib/view_model/dashboard/transaction_list_item.dart
@@ -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';
### lib/view_model/wallet_account_list/account_edit_or_create_view_model.dart
@@ -0,0 +1,9 @@
+import 'package:cake_wallet/core/execution_state.dart';
+
+abstract class WalletAccountEditOrCreateViewModel {
+ bool get isEdit;
+ ExecutionState get state;
+ String get label;
+ set label(String value);
+ Future<void> save();
+}
### lib/view_model/wallet_account_list/account_list_item.dart
[binary or diff unavailable]
### lib/view_model/wallet_account_list/bitcoin_account_list/bitcoin_account_edit_or_create_view_model.dart
@@ -0,0 +1,92 @@
+import 'dart:math';
+
+import 'package:cake_wallet/bitcoin/bitcoin.dart';
+import 'package:cake_wallet/core/execution_state.dart';
+import 'package:cake_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart';
+import 'package:cw_core/balance_card_style_settings.dart';
+import 'package:cw_core/card_design.dart';
+import 'package:cw_core/wallet_base.dart';
+import 'package:flutter/material.dart';
+import 'package:mobx/mobx.dart';
+
+part 'bitcoin_account_edit_or_create_view_model.g.dart';
+
+class BitcoinAccountEditOrCreateViewModel = BitcoinAccountEditOrCreateViewModelBase
+ with _$BitcoinAccountEditOrCreateViewModel;
+
+abstract class BitcoinAccountEditOrCreateViewModelBase
+ with Store
+ implements WalletAccountEditOrCreateViewModel {
+ BitcoinAccountEditOrCreateViewModelBase({required WalletBase wallet})
+ : state = InitialExecutionState(),
+ label = "",
+ _wallet = wallet;
+
+ @override
+ bool get isEdit => false;
+
+ @override
+ @observable
+ ExecutionState state;
+
+ @override
+ @observable
+ String label;
+
+ final WalletBase _wallet;
+
+ Future<List<Gradient>> _getUsableCardGradients() async {
+ final List<Gradient> ret = List<Gradient>.from(CardDesign.allGradients);
+ final designs = (await BalanceCardStyleSettings.getAll(_wallet.walletInfo.internalId))
+ .map((item) => CardDesign.fromStyleSettings(item, _wallet.currency));
+ for (final design in designs) {
+ ret.remove(design.gradient);
+ }
+ return ret.isNotEmpty ? ret : CardDesign.allGradients;
+ }
+
+ Future<void> _saveRandomCardDesign(int accountIndex) async {
+ final gradients = await _getUsableCardGradients();
+ final accounts = await _wallet.walletInfo.getAccounts();
+
+ await BalanceCardStyleSettings.fromCardDesign(
+ walletInfoId: _wallet.walletInfo.internalId,
+ accountIndex: accountIndex,
+ cardOrder: accounts.length - 1,
+ design: CardDesign.specialDesignsForCurrencies[_wallet.currency]!
+ .withGradient(gradients[Random().nextInt(gradients.length)]))
+ .insert();
+ }
+
+ @override
+ Future<void> save() async {
+ try {
+ state = IsExecutingState();
+
+ final accountIndex = await _createAccount(label);
+ await _saveRandomCardDesign(accountIndex);
+
+ await _wallet.save();
+ state = ExecutedSuccessfullyState();
+ } catch (e) {
+ state = FailureState(e.toString());
+ }
+ }
+
+ Future<int> _createAccount(String label) async {
+ final accounts = await _wallet.walletInfo.getAccounts();
+
+ final nextIndex = accounts.isEmpty
+ ? 0
+ : accounts.map((e) => e.accountIndex).reduce((a, b) => a > b ? a : b) + 1;
+
+ await _wallet.walletInfo.addAccount(
+ accountIndex: nextIndex,
+ label: label.isEmpty ? "Account $nextIndex" : label,
+ );
+
+ await bitcoin!.setCurrentAccount(_wallet, nextIndex);
+
+ return nextIndex;
+ }
+}
### lib/view_model/wallet_account_list/bitcoin_account_list/bitcoin_account_list_view_model.dart
@@ -0,0 +1,130 @@
+import 'dart:async';
+
+import 'package:cake_wallet/bitcoin/bitcoin.dart';
+import 'package:cake_wallet/core/amount_parsing_proxy.dart';
+import 'package:cake_wallet/entities/balance_display_mode.dart';
+import 'package:cake_wallet/store/settings_store.dart';
+import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
+import 'package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart';
+import 'package:collection/collection.dart';
+import 'package:cw_core/crypto_currency.dart';
+import 'package:cw_core/wallet_base.dart';
+import 'package:cw_core/wallet_info.dart';
+import 'package:mobx/mobx.dart';
+
+part 'bitcoin_account_list_view_model.g.dart';
+
+class BitcoinAccountListViewModel = BitcoinAccountListViewModelBase
+ with _$BitcoinAccountListViewModel;
+
+abstract class BitcoinAccountListViewModelBase with Store implements WalletAccountListViewModel {
+ BitcoinAccountListViewModelBase(this._wallet, this.settingsStore) {
+ unawaited(_loadAccounts());
+
+ reaction(
+ (_) => bitcoin!.accountBalancesSnapshot(_wallet),
+ (snapshot) => unawaited(_refreshBalances(snapshot)),
+ equals: const MapEquality<int, Object>().equals,
+ );
+
+ reaction(
+ (_) => settingsStore.balanceDisplayMode,
+ (_) => unawaited(_recomputeBalanceStrings()),
+ );
+ }
+
+ final SettingsStore settingsStore;
+ final WalletBase _wallet;
+
+ List<WalletInfoAccount>? _cachedStoredAccounts;
+ Future<void>? _loadAccountsFuture;
+
+ CryptoCurrency get currency => _wallet.currency;
+
+ @override
+ @observable
+ ObservableList<AccountListItem> accounts = ObservableList<AccountListItem>();
+
+ @override
+ @observable
+ AccountListItem? selectedAccount;
+
+ @override
+ @action
+ Future<void> select(AccountListItem account) async {
+ await bitcoin!.setCurrentAccount(_wallet, account.id);
+ runInAction(() => selectedAccount = account);
+ await reload();
+ }
+
+ @action
+ Future<void> _loadAccounts() => _runLoad(() => _wallet.walletInfo.getAccounts());
+
+ @action
+ Future<void> _refreshBalances(Map<int, Object> balancesSnapshot) {
+ final cached = _cachedStoredAccounts;
+ final hasUnknownAccount = cached == null ||
+ balancesSnapshot.keys
+ .any((accountIndex) => !cached.any((a) => a.accountIndex == accountIndex));
+
+ if (hasUnknownAccount) return _loadAccounts();
+ return _runLoad(() async => cached);
+ }
+
+
+ @action
+ Future<void> _recomputeBalanceStrings() {
+ final cached = _cachedStoredAccounts;
+ if (cached == null) return _loadAccounts();
+ return _runLoad(() async => cached);
+ }
+
+ Future<void> _runLoad(Future<List<WalletInfoAccount>> Function() fetch) {
+ final inProgress = _loadAccountsFuture;
+ if (inProgress != null) {
+ return inProgress;
+ }
+
+ final future = _applyAccounts(fetch);
+ _loadAccountsFuture = future;
+ return future.whenComplete(() => _loadAccountsFuture = null);
+ }
+
+ Future<void> _applyAccounts(Future<List<WalletInfoAccount>> Function() fetch) async {
+ final storedAccounts = await fetch();
+ _cachedStoredAccounts = storedAccounts;
+
+ final currentAccountIndex = _wallet.walletInfo.currentAccountIndex;
+ final items = storedAccounts
+ .map((account) => AccountListItem(
+ id: account.accountIndex,
+ label: account.label,
+ balance: _balanceForAccount(account.accountIndex),
+ isSelected: account.accountIndex == currentAccountIndex,
+ ))
+ .toList();
+
+ runInAction(() {
+ accounts = ObservableList<AccountListItem>.of(items);
+ selectedAccount = items.isEmpty
+ ? null
+ : items.firstWhere(
+ (account) => account.isSelected,
+ orElse: () => items.first,
+ );
+ });
+ }
+
+ @override
+ @action
+ Future<void> reload() => _loadAccounts();
+
+ String _balanceForAccount(int accountIndex) {
+ if (settingsStore.balanceDisplayMode == BalanceDisplayMode.hiddenBalance) {
+ return '●●●●●●';
+ }
+ final balance = bitcoin!.balanceForAccount(_wallet, accountIndex);
+
+ return AmountParsingProxy(settingsStore.displayAmountsInSatoshi).asDisplayString(balance.available);
+ }
+}
### lib/view_model/wallet_account_list/monero_account_list/monero_account_edit_or_create_view_model.dart
@@ -1,5 +1,6 @@
import 'dart:math';
+import 'package:cake_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart';
import 'package:cake_wallet/wownero/wownero.dart';
import 'package:cw_core/balance_card_style_settings.dart';
import 'package:cw_core/card_design.dart';
@@ -9,14 +10,16 @@ import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/monero/monero.dart';
-import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
+import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
part 'monero_account_edit_or_create_view_model.g.dart';
class MoneroAccountEditOrCreateViewModel = MoneroAccountEditOrCreateViewModelBase
with _$MoneroAccountEditOrCreateViewModel;
-abstract class MoneroAccountEditOrCreateViewModelBase with Store {
+abstract class MoneroAccountEditOrCreateViewModelBase
+ with Store
+ implements WalletAccountEditOrCreateViewModel {
MoneroAccountEditOrCreateViewModelBase(this._moneroAccountList, this._wowneroAccountList,
{required WalletBase wallet, AccountListItem? accountListItem})
: state = InitialExecutionState(),
@@ -61,25 +64,15 @@ abstract class MoneroAccountEditOrCreateViewModelBase with Store {
}
Future<void> save() async {
- await _saveRandomCardDesign();
- if (_wallet.type == WalletType.monero) {
- await saveMonero();
- }
-
- if (_wallet.type == WalletType.wownero) {
- await saveWownero();
- }
- }
-
- Future<void> saveMonero() async {
try {
state = IsExecutingState();
- if (_accountListItem != null) {
- await _moneroAccountList.setLabelAccount(_wallet,
- accountIndex: _accountListItem.id, label: label);
- } else {
- await _moneroAccountList.addAccount(_wallet, label: label);
+ if (!isEdit) await _saveRandomCardDesign();
+
+ if (_wallet.type == WalletType.monero) {
+ await saveMonero();
+ } else if (_wallet.type == WalletType.wownero) {
+ await saveWownero();
}
await _wallet.save();
@@ -89,21 +82,33 @@ abstract class MoneroAccountEditOrCreateViewModelBase with Store {
}
}
- Future<void> saveWownero() async {
- try {
- state = IsExecutingState();
-
- if (_accountListItem != null) {
- await _wowneroAccountList?.setLabelAccount(_wallet,
- accountIndex: _accountListItem.id, label: label);
- } else {
- await _wowneroAccountList?.addAccount(_wallet, label: label);
- }
+ Future<void> saveMonero() async {
+ if (_accountListItem != null) {
+ await _moneroAccountList.setLabelAccount(
+ _wallet,
+ accountIndex: _accountListItem.id,
+ label: label,
+ );
+ } else {
+ await _moneroAccountList.addAccount(
+ _wallet,
+ label: label,
+ );
+ }
+ }
- await _wallet.save();
- state = ExecutedSuccessfullyState();
- } catch (e) {
- state = FailureState(e.toString());
+ Future<void> saveWownero() async {
+ if (_accountListItem != null) {
+ await _wowneroAccountList?.setLabelAccount(
+ _wallet,
+ accountIndex: _accountListItem.id,
+ label: label,
+ );
+ } else {
+ await _wowneroAccountList?.addAccount(
+ _wallet,
+ label: label,
+ );
}
}
}
### lib/view_model/wallet_account_list/monero_account_list/monero_account_list_view_model.dart
@@ -1,29 +1,25 @@
import 'package:cake_wallet/entities/balance_display_mode.dart';
import 'package:cake_wallet/store/settings_store.dart';
+import 'package:cake_wallet/view_model/wallet_account_list/wallet_account_list_view_model.dart';
import 'package:cake_wallet/wownero/wownero.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_base.dart';
-import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
+import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
import 'package:cake_wallet/monero/monero.dart';
part 'monero_account_list_view_model.g.dart';
class MoneroAccountListViewModel = MoneroAccountListViewModelBase with _$MoneroAccountListViewModel;
-abstract class MoneroAccountListViewModelBase with Store {
- MoneroAccountListViewModelBase(this._wallet, this.settingsStore) : scrollOffsetFromTop = 0;
+abstract class MoneroAccountListViewModelBase with Store implements WalletAccountListViewModel {
+ MoneroAccountListViewModelBase(this._wallet, this.settingsStore);
final SettingsStore settingsStore;
- @observable
- double scrollOffsetFromTop;
-
- @action
- void setScrollOffsetFromTop(double scrollOffsetFromTop) {
- this.scrollOffsetFromTop = scrollOffsetFromTop;
- }
+ @override
+ AccountListItem? get selectedAccount => selected;
CryptoCurrency get currency => _wallet.currency;
@@ -65,7 +61,11 @@ abstract class MoneroAccountListViewModelBase with Store {
final WalletBase _wallet;
- void select(AccountListItem item) {
+ @override
+ Future<void> reload() async {}
+
+ @override
+ Future<void> select(AccountListItem item) async {
if (_wallet.type == WalletType.monero) {
monero!.setCurrentAccount(
_wallet,
### lib/view_model/wallet_account_list/nano_account_list/nano_account_edit_or_create_view_model.dart
[binary or diff unavailable]
### lib/view_model/wallet_account_list/nano_account_list/nano_account_list_view_model.dart
[binary or diff unavailable]
### lib/view_model/wallet_account_list/wallet_account_list_view_model.dart
@@ -0,0 +1,14 @@
+import 'package:cake_wallet/view_model/wallet_account_list/account_list_item.dart';
+import 'package:cw_core/crypto_currency.dart';
+
+abstract class WalletAccountListViewModel {
+ List<AccountListItem> get accounts;
+
+ AccountListItem? get selectedAccount;
+
+ CryptoCurrency get currency;
+
+ Future<void> select(AccountListItem account);
+
+ Future<void> reload();
+}
### res/pictures/settings_row_icons/accounts.svg
@@ -1,27 +1,28 @@
-<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g id="Setttings Icons" clip-path="url(#clip0_0_342)">
-<path id="bg" d="M0 8C0 5.19974 0 3.79961 0.544967 2.73005C1.02433 1.78924 1.78924 1.02433 2.73005 0.544967C3.79961 0 5.19974 0 8 0H16C18.8003 0 20.2004 0 21.27 0.544967C22.2108 1.02433 22.9757 1.78924 23.455 2.73005C24 3.79961 24 5.19974 24 8V16C24 18.8003 24 20.2004 23.455 21.27C22.9757 22.2108 22.2108 22.9757 21.27 23.455C20.2004 24 18.8003 24 16 24H8C5.19974 24 3.79961 24 2.73005 23.455C1.78924 22.9757 1.02433 22.2108 0.544967 21.27C0 20.2004 0 18.8003 0 16V8Z" fill="url(#paint0_linear_0_342)"/>
-<g id="BUTTON">
-<g id="Union">
-<path d="M16.4007 4.38126C16.7865 4.38126 17.1565 4.53448 17.4293 4.80725C17.7021 5.08003 17.8553 5.45008 17.8553 5.83584H6.21905C6.21905 5.45007 6.37227 5.08003 6.64505 4.80725C6.91782 4.5345 7.28776 4.38126 7.6735 4.38126H16.4007Z" fill="url(#paint1_linear_0_342)"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0371 17.4313C20.0371 17.8171 19.8839 18.1871 19.6111 18.4599C19.3383 18.7327 18.9683 18.8859 18.5825 18.8859H5.49169C5.10595 18.8859 4.73601 18.7327 4.46324 18.4599C4.19046 18.1871 4.03711 17.8171 4.03711 17.4313V8.74487C4.03711 8.35911 4.19047 7.9892 4.46324 7.71642C4.73602 7.44365 5.10594 7.29029 5.49169 7.29028H18.5825C18.9683 7.29028 19.3383 7.44364 19.6111 7.71642C19.8839 7.98919 20.0371 8.35913 20.0371 8.74487V17.4313ZM5.49169 17.4313H18.5825V8.74487H5.49169V17.4313Z" fill="url(#paint2_linear_0_342)"/>
-</g>
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_20104_177116)">
+<g clip-path="url(#clip1_20104_177116)">
+<path d="M0 8C0 5.19974 0 3.79961 0.544967 2.73005C1.02433 1.78924 1.78924 1.02433 2.73005 0.544967C3.79961 0 5.19974 0 8 0H16C18.8003 0 20.2004 0 21.27 0.544967C22.2108 1.02433 22.9757 1.78924 23.455 2.73005C24 3.79961 24 5.19974 24 8V16C24 18.8003 24 20.2004 23.455 21.27C22.9757 22.2108 22.2108 22.9757 21.27 23.455C20.2004 24 18.8003 24 16 24H8C5.19974 24 3.79961 24 2.73005 23.455C1.78924 22.9757 1.02433 22.2108 0.544967 21.27C0 20.2004 0 18.8003 0 16V8Z" fill="url(#paint0_linear_20104_177116)"/>
+<path d="M16.4027 4.38123C16.7884 4.38123 17.1585 4.53445 17.4313 4.80722C17.704 5.08 17.8573 5.45005 17.8573 5.83581H6.221C6.221 5.45004 6.37423 5.08 6.647 4.80722C6.91977 4.53447 7.28971 4.38123 7.67545 4.38123H16.4027Z" fill="url(#paint1_linear_20104_177116)"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0391 17.4313C20.0391 17.8171 19.8858 18.1871 19.6131 18.4599C19.3403 18.7327 18.9702 18.8859 18.5845 18.8859H5.49365C5.1079 18.8859 4.73797 18.7326 4.4652 18.4599C4.19242 18.1871 4.03906 17.8171 4.03906 17.4313V8.74484C4.03906 8.35908 4.19243 7.98917 4.4652 7.71639C4.73797 7.44362 5.10789 7.29026 5.49365 7.29025H18.5845C18.9702 7.29025 19.3403 7.44361 19.6131 7.71639C19.8858 7.98916 20.0391 8.3591 20.0391 8.74484V17.4313ZM5.49365 17.4313H18.5845V8.74484H5.49365V17.4313Z" fill="url(#paint2_linear_20104_177116)"/>
</g>
</g>
<defs>
-<linearGradient id="paint0_linear_0_342" x1="12" y1="0" x2="12" y2="24" gradientUnits="userSpaceOnUse">
+<linearGradient id="paint0_linear_20104_177116" x1="12" y1="0" x2="12" y2="24" gradientUnits="userSpaceOnUse">
<stop stop-color="#009393"/>
<stop offset="1" stop-color="#005173"/>
</linearGradient>
-<linearGradient id="paint1_linear_0_342" x1="12.0371" y1="4.38126" x2="12.0371" y2="18.8859" gradientUnits="userSpaceOnUse">
+<linearGradient id="paint1_linear_20104_177116" x1="12.0391" y1="4.38123" x2="12.0391" y2="18.8859" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#88FCFF"/>
</linearGradient>
-<linearGradient id="paint2_linear_0_342" x1="12.0371" y1="4.38126" x2="12.0371" y2="18.8859" gradientUnits="userSpaceOnUse">
+<linearGradient id="paint2_linear_20104_177116" x1="12.0391" y1="4.38123" x2="12.0391" y2="18.8859" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#88FCFF"/>
</linearGradient>
-<clipPath id="clip0_0_342">
+<clipPath id="clip0_20104_177116">
+<rect width="24" height="24" fill="white"/>
+</clipPath>
+<clipPath id="clip1_20104_177116">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
### res/values/strings_ar.arb
@@ -238,8 +238,6 @@
"color_theme": "سمة الألوان",
"coming_soon_tag": "قريبًا",
"commit_transaction_amount_fee": "تأكيد المعاملة\nالمبلغ: ${amount}\nالرسوم: ${fee}",
- "compatible_services": "الخدمات المتوافقة",
- "compatible_services_desc": "استخدم أداة المسح لقراءة الأنواع التالية من رموز QR المدعومة في Cake Wallet:",
"compatible_networks": "الشبكات المتوافقة",
"compatible_services": "الخدمات المتوافقة",
"compatible_services_desc": "استخدم أداة المسح لقراءة الأنواع التالية من رموز QR المدعومة في Cake Wallet:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "حدث خطأ أثناء جلب رموز NFT. يُرجى التحقق من اتصالك بالإنترنت والمحاولة مرة أخرى.",
"more_details": "مزيد من التفاصيل",
"more_options": "مزيد من الخيارات",
+ "multiple_accounts": "حسابات متعددة",
"multiple_addresses_detected": "تم اكتشاف عدة عناوين",
"mweb_ad": "فعّل MWEB، طبقة الخصوصية لـ Litecoin التي تضيف مزيدًا من إخفاء الهوية لعملاتك",
"mweb_confirmed": "MWEB مؤكَّد",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "جميع أحدث النماذج (GPT-5.3، Claude 4.6).\nبدون اشتراك، ادفع بالعملات المشفرة.",
"narrow": "ضيق",
"network": "الشبكة",
- "networks": "الشبكات",
"network_address_detected": "تم اكتشاف عنوان ${network}.",
"network_prefix_on": "على",
"network_switch_failed": "فشل التبديل إلى الشبكة المطلوبة. لم يتم تطبيق الدفع، يرجى المحاولة مرة أخرى.",
@@ -1581,4 +1579,4 @@
"zcash_network_upgrade_desc_1": "خضعت Zcash لترقية شبكة تسمى Ironwood والتي تتطلب ترحيل الأموال قبل أن يتم إنفاقها. هذا الترحيل تلقائي، ويحافظ على الخصوصية، ويبدأ بمجرد فتح محفظة Zcash الخاصة بك.",
"zcash_network_upgrade_desc_2": "عند انتهاء عملية الترحيل، سيعود رصيدك المتاح إلى وضعه الطبيعي، ويمكنك الاستمرار في إرسال واستقبال Zcash كالمعتاد.",
"zcash_network_upgrade_desc_3": "يرجى السماح لمحفظة Zcash الخاصة بك بإنهاء المزامنة، ثم اترك التطبيق مفتوحًا حتى انتهاء الترحيل، الأمر الذي قد يستغرق بضع دقائق حسب الرصيد."
-}
+}
\ No newline at end of file
### res/values/strings_bg.arb
@@ -238,8 +238,6 @@
"color_theme": "Цветова тема",
"coming_soon_tag": "Очаквайте скоро",
"commit_transaction_amount_fee": "Потвърдете транзакцията\nСума: ${amount}\nТакса: ${fee}",
- "compatible_services": "Съвместими услуги",
- "compatible_services_desc": "Използвайте инструмента за сканиране, за да четете следните типове QR кодове, поддържани в Cake Wallet:",
"compatible_networks": "Съвместими мрежи",
"compatible_services": "Съвместими услуги",
"compatible_services_desc": "Използвайте инструмента за сканиране, за да четете следните типове QR кодове, поддържани в Cake Wallet:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "Възникна грешка при извличането на NFT. Моля, проверете интернет връзката си и опитайте отново.",
"more_details": "Повече подробности",
"more_options": "Още опции",
+ "multiple_accounts": "Множество сметки",
"multiple_addresses_detected": "Открити са множество адреси",
"mweb_ad": "Включете MWEB – слоя за поверителност на Litecoin, който добавя допълнителна анонимност към вашите монети",
"mweb_confirmed": "Потвърдено MWEB",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "Всички най-нови модели (GPT-5.3, Claude 4.6).\\nБез абонамент, плащайте с крипто.",
"narrow": "Тесен",
"network": "Мрежа",
- "networks": "Мрежи",
"network_address_detected": "Открит адрес на ${network}",
"network_prefix_on": "на",
"network_switch_failed": "Неуспешно превключване към заявената мрежа. Плащането не е приложено, моля опитайте отново.",
@@ -1581,4 +1579,4 @@
"zcash_network_upgrade_desc_1": "Zcash претърпя надграждане на мрежата, наречено Ironwood, което изисква миграция на средства, преди да могат да бъдат изразходвани. Тази миграция е автоматична, запазвайки поверителността и започва веднага щом отворите Zcash портфейла си.",
"zcash_network_upgrade_desc_2": "Когато миграцията приключи, вашият наличен баланс ще се върне към нормалното и можете да продължите да изпращате и получавате Zcash както обикновено.",
"zcash_network_upgrade_desc_3": "Моля, оставете вашия Zcash портфейл да завърши синхронизирането и след това оставете приложението отворено, докато миграцията приключи, което може да отнеме няколко минути в зависимост от баланса."
-}
+}
\ No newline at end of file
### res/values/strings_cs.arb
@@ -238,8 +238,6 @@
"color_theme": "Barevné téma",
"coming_soon_tag": "Již brzy",
"commit_transaction_amount_fee": "Potvrdit transakci\nČástka: ${amount}\nPoplatek: ${fee}",
- "compatible_services": "Kompatibilní služby",
- "compatible_services_desc": "Pomocí nástroje pro skenování načtěte následující typy QR kódů podporované v Cake Wallet:",
"compatible_networks": "Kompatibilní sítě",
"compatible_services": "Kompatibilní služby",
"compatible_services_desc": "Pomocí nástroje pro skenování načtěte následující typy QR kódů podporované v Cake Wallet:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "Při načítání NFT došlo k chybě. Zkontrolujte prosím připojení k internetu a zkuste to znovu.",
"more_details": "Více podrobností",
"more_options": "Více možností",
+ "multiple_accounts": "Více účtů",
"multiple_addresses_detected": "Detekováno více adres",
"mweb_ad": "Povolte MWEB, vrstvu soukromí pro Litecoin, která vašim mincím přidává další anonymitu",
"mweb_confirmed": "Potvrzené MWEB",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "Všechny nejnovější modely (GPT-5.3, Claude 4.6).\nBez předplatného, plaťte kryptem.",
"narrow": "Úzké",
"network": "Síť",
- "networks": "Sítě",
"network_address_detected": "Zjištěna adresa ${network}",
"network_prefix_on": "na",
"network_switch_failed": "Přepnutí na požadovanou síť se nezdařilo. Platba nebyla připsána, zkuste to prosím znovu.",
@@ -1581,4 +1579,4 @@
"zcash_network_upgrade_desc_1": "Zcash prošel upgradem sítě nazvaným Ironwood, který vyžaduje migraci finančních prostředků, než je lze utratit. Tato migrace je automatická, zachovává soukromí a začíná, jakmile otevřete peněženku Zcash.",
"zcash_network_upgrade_desc_2": "Po dokončení migrace se váš disponibilní zůstatek vrátí do normálu a můžete nadále odesílat a přijímat Zcash jako obvykle.",
"zcash_network_upgrade_desc_3": "Nechte prosím dokončit synchronizaci vaší peněženky Zcash a poté nechte aplikaci otevřenou, dokud nebude migrace dokončena, což může trvat několik minut v závislosti na zůstatku."
-}
+}
\ No newline at end of file
### res/values/strings_de.arb
@@ -239,8 +239,6 @@
"color_theme": "Farbschema",
"coming_soon_tag": "Demnächst verfügbar",
"commit_transaction_amount_fee": "Transaktion bestätigen\nBetrag: ${amount}\nGebühr: ${fee}",
- "compatible_services": "Kompatible Dienste",
- "compatible_services_desc": "Verwenden Sie das Scan-Tool, um die folgenden von Cake Wallet unterstützten QR-Code-Typen zu lesen:",
"compatible_networks": "Kompatible Netzwerke",
"compatible_services": "Kompatible Dienste",
"compatible_services_desc": "Verwenden Sie das Scan-Tool, um die folgenden von Cake Wallet unterstützten QR-Code-Typen zu lesen:",
@@ -701,6 +699,7 @@
"moralis_nft_error": "Beim Abrufen von NFTs ist ein Fehler aufgetreten. Bitte überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.",
"more_details": "Weitere Details",
"more_options": "Weitere Optionen",
+ "multiple_accounts": "Mehrere Konten",
"multiple_addresses_detected": "Mehrere Adressen erkannt",
"mweb_ad": "Aktivieren Sie MWEB, die Datenschutzebene für Litecoin, die Ihren Coins zusätzliche Anonymität verleiht",
"mweb_confirmed": "Bestätigtes MWEB",
@@ -715,7 +714,6 @@
"nanogpt_subtitle": "Alle neuesten Modelle (GPT-5.3, Claude 4.6).\nKein Abonnement – bezahlen Sie mit Krypto.",
"narrow": "Schmal",
"network": "Netzwerk",
- "networks": "Netzwerke",
"network_address_detected": "${network}-Adresse erkannt",
"network_prefix_on": "auf",
"network_switch_failed": "Der Wechsel zum angeforderten Netzwerk ist fehlgeschlagen. Die Zahlung wurde nicht ausgeführt. Bitte versuchen Sie es erneut.",
@@ -803,7 +801,6 @@
"pairing_error": "Kopplungsfehler",
"pairingInvalidEvent": "Ungültiges Pairing-Ereignis",
"passphrase": "Passphrase (Optional)",
- "passphrase_view_keys": "Passphrase",
"passphrase_entry": "Passphrase-Eingabe",
"passphrase_raw": "Passphrase",
"passphrase_view_keys": "Passphrase",
@@ -1580,9 +1577,9 @@
"zcash_card_enable_later": "Sie können diese Karte jederzeit später in den Einstellungen aktivieren",
"zcash_card_missing_funds": "Fehlendes Guthaben?",
"zcash_card_scan": "Scannen",
+ "zcash_card_warning": "Schließen Sie die App nicht, bis der Vorgang abgeschlossen ist. Andernfalls muss der Vorgang von Grund auf neu gestartet werden.",
"zcash_network_upgrade": "Zcash-Netzwerk-Upgrade",
"zcash_network_upgrade_desc_1": "Das Zcash-Netzwerk wurde auf Ironwood aktualisiert. Damit Ihr Guthaben wieder ausgegeben werden kann, muss es migriert werden. Die Migration erfolgt automatisch und unter Wahrung Ihrer Privatsphäre. Sie beginnt, sobald Sie Ihre Zcash-Wallet öffnen.",
"zcash_network_upgrade_desc_2": "Nach Abschluss der Migration wird Ihr verfügbares Guthaben wieder wie gewohnt angezeigt und Sie können Zcash wieder normal senden und empfangen.",
- "zcash_network_upgrade_desc_3": "Bitte lassen Sie Ihre Zcash-Wallet vollständig synchronisieren und anschließend die App geöffnet, bis die Migration abgeschlossen ist. Dies kann je nach Guthaben einige Minuten dauern.",
- "zcash_card_warning": "Schließen Sie die App nicht, bis der Vorgang abgeschlossen ist. Andernfalls muss der Vorgang von Grund auf neu gestartet werden."
-}
+ "zcash_network_upgrade_desc_3": "Bitte lassen Sie Ihre Zcash-Wallet vollständig synchronisieren und anschließend die App geöffnet, bis die Migration abgeschlossen ist. Dies kann je nach Guthaben einige Minuten dauern."
+}
\ No newline at end of file
### res/values/strings_en.arb
@@ -270,8 +270,8 @@
"connect_your_hardware_wallet_ble": "Connect your hardware wallet using Bluetooth",
"connect_your_hardware_wallet_usb": "Connect your hardware wallet using USB",
"connected": "Connected",
- "connection_sync": "Connection and sync",
"connecting_you_to": "Connecting you to",
+ "connection_sync": "Connection and sync",
"connections": "Connections",
"connections_desc": "Manage connections to services and third party APIs.",
"connectWalletPrompt": "Connect your wallet with WalletConnect to make transactions",
@@ -706,6 +706,7 @@
"moralis_nft_error": "An error occurred while fetching NFTs. Kindly check your internet connection and try again.",
"more_details": "More Details",
"more_options": "More Options",
+ "multiple_accounts": "Multiple Accounts",
"multiple_addresses_detected": "Multiple addresses detected",
"mweb_ad": "Enable MWEB, the privacy layer for Litecoin that adds extra anonymity to your coins",
"mweb_confirmed": "Confirmed MWEB",
@@ -879,8 +880,8 @@
"privacy_settings": "Privacy settings",
"private_key": "Private key",
"private_memo_optional": "Private Memo (optional)",
- "proceed_after_one_minute": "If the screen doesn’t proceed after 1 minute, check your email.",
"proceed": "Proceed",
+ "proceed_after_one_minute": "If the screen doesn’t proceed after 1 minute, check your email.",
"proceed_on_device": "Proceed on your device",
"proceed_on_device_description": "Please follow the instructions prompted on your hardware wallet",
"processing": "Processing",
@@ -1431,9 +1432,9 @@
"unsupported_asset": "We don't support this action for this asset. Please create or switch to a wallet of a supported asset type.",
"unsupported_network_requested": "This payment request is for a network we don't support (chain ID ${chainId}).",
"unsupported_token_requested": "This payment request is for a token we couldn't identify on the selected network.",
+ "up_to": "Up to",
"uptime": "Uptime",
"upto": "up to ${value}",
- "up_to": "Up to",
"usb": "USB",
"use": "Switch to ",
"use_blink_protection": "Use Blink Protection",
@@ -1587,4 +1588,4 @@
"zcash_network_upgrade_desc_1": "Zcash has undergone a network upgrade called Ironwood that requires migration of funds before they can be spent. This migration is automatic, privacy-preserving, and starts as soon as you open your Zcash wallet.",
"zcash_network_upgrade_desc_2": "When the migration finishes, your Available Balance will return to normal, and you can continue to send and receive Zcash as usual.",
"zcash_network_upgrade_desc_3": "Please allow your Zcash wallet to finishing syncing, and then leave the app open until migration finishes which can take a few minutes depending on the balance."
-}
+}
\ No newline at end of file
### res/values/strings_es.arb
@@ -238,8 +238,6 @@
"color_theme": "Tema de color",
"coming_soon_tag": "Próximamente",
"commit_transaction_amount_fee": "Confirmar transacción\nCantidad: ${amount}\nComisión: ${fee}",
- "compatible_services": "Servicios compatibles",
- "compatible_services_desc": "Usa la herramienta Escanear para leer los siguientes tipos de códigos QR compatibles con Cake Wallet:",
"compatible_networks": "Redes compatibles",
"compatible_services": "Servicios compatibles",
"compatible_services_desc": "Usa la herramienta Escanear para leer los siguientes tipos de códigos QR compatibles con Cake Wallet:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "Se produjo un error al obtener los NFT. Por favor, verifica tu conexión a Internet y vuelve a intentarlo.",
"more_details": "Más detalles",
"more_options": "Más opciones",
+ "multiple_accounts": "Varias cuentas",
"multiple_addresses_detected": "Se detectaron varias direcciones",
"mweb_ad": "Habilita MWEB, la capa de privacidad de Litecoin que añade anonimato adicional a tus monedas",
"mweb_confirmed": "MWEB confirmado",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "Todos los modelos más recientes (GPT-5.3, Claude 4.6).\nSin suscripción, paga con cripto.",
"narrow": "Estrecho",
"network": "Red",
- "networks": "Redes",
"network_address_detected": "${network} dirección detectada",
"network_prefix_on": "en",
"network_switch_failed": "No se pudo cambiar a la red solicitada. El pago no se aplicó, inténtelo nuevamente.",
@@ -1583,4 +1581,4 @@
"zcash_network_upgrade_desc_1": "Zcash ha experimentado una actualización de red llamada Ironwood que requiere la migración de fondos antes de que puedan gastarse. Esta migración es automática, preserva la privacidad y comienza tan pronto como abre su billetera Zcash.",
"zcash_network_upgrade_desc_2": "Cuando finalice la migración, su Saldo disponible volverá a la normalidad y podrá continuar enviando y recibiendo Zcash como de costumbre.",
"zcash_network_upgrade_desc_3": "Permita que su billetera Zcash termine de sincronizarse y luego deje la aplicación abierta hasta que finalice la migración, lo que puede demorar unos minutos dependiendo del saldo."
-}
+}
\ No newline at end of file
### res/values/strings_fa.arb
@@ -238,8 +238,6 @@
"color_theme": "تم رنگ",
"coming_soon_tag": "بهزودی",
"commit_transaction_amount_fee": "ثبت تراکنش\nمقدار: ${amount}\nکارمزد: ${fee}",
- "compatible_services": "سرویسهای سازگار",
- "compatible_services_desc": "از ابزار اسکن برای خواندن انواع کدهای QR زیر که در Cake Wallet پشتیبانی میشوند استفاده کنید:",
"compatible_networks": "شبکه های سازگار",
"compatible_services": "سرویسهای سازگار",
"compatible_services_desc": "از ابزار اسکن برای خواندن انواع کدهای QR زیر که در Cake Wallet پشتیبانی میشوند استفاده کنید:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "هنگام دریافت NFTها خطایی رخ داد.\nلطفاً اتصال اینترنت خود را بررسی کنید و دوباره تلاش کنید.",
"more_details": "جزئیات بیشتر",
"more_options": "گزینههای بیشتر",
+ "multiple_accounts": "حساب های چندگانه",
"multiple_addresses_detected": "چندین آدرس شناسایی شد",
"mweb_ad": "MWEB را فعال کنید؛ لایهٔ حریم خصوصی لایتکوین که ناشناسبودن بیشتری به کوینهای شما اضافه میکند",
"mweb_confirmed": "MWEB تأییدشده",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "همه جدیدترین مدلها (GPT-5.3, Claude 4.6).\nبدون اشتراک، پرداخت با کریپتو.",
"narrow": "باریک",
"network": "شبکه",
- "networks": "شبکهها",
"network_address_detected": "آدرس ${network} شناسایی شد",
"network_prefix_on": "در",
"network_switch_failed": "تغییر به شبکه درخواستی انجام نشد. پرداخت اعمال نشد، لطفاً دوباره امتحان کنید.",
@@ -1581,4 +1579,4 @@
"zcash_network_upgrade_desc_1": "Zcash تحت یک ارتقاء شبکه به نام Ironwood قرار گرفته است که قبل از خرج کردن، نیاز به انتقال سرمایه دارد. این انتقال خودکار است و حریم خصوصی را حفظ می کند و به محض باز کردن کیف پول Zcash خود شروع می شود.",
"zcash_network_upgrade_desc_2": "هنگامی که انتقال به پایان رسید، موجودی موجود شما به حالت عادی باز می گردد و می توانید طبق معمول به ارسال و دریافت Zcash ادامه دهید.",
"zcash_network_upgrade_desc_3": "لطفاً به کیف پول Zcash خود اجازه دهید تا همگامسازی تمام شود، و سپس برنامه را باز بگذارید تا انتقال به پایان برسد که بسته به موجودی میتواند چند دقیقه طول بکشد."
-}
+}
\ No newline at end of file
### res/values/strings_fr.arb
@@ -238,8 +238,6 @@
"color_theme": "Thème de couleur",
"coming_soon_tag": "Bientôt disponible",
"commit_transaction_amount_fee": "Confirmer la transaction\nMontant : ${amount}\nFrais : ${fee}",
- "compatible_services": "Services compatibles",
- "compatible_services_desc": "Utilisez l'outil Scan pour lire les types de codes QR suivants pris en charge par Cake Wallet :",
"compatible_networks": "Réseaux compatibles",
"compatible_services": "Services compatibles",
"compatible_services_desc": "Utilisez l'outil Scan pour lire les types de codes QR suivants pris en charge par Cake Wallet :",
@@ -699,6 +697,7 @@
"moralis_nft_error": "Une erreur s'est produite lors de la récupération des NFT. Veuillez vérifier votre connexion Internet et réessayer.",
"more_details": "Plus de détails",
"more_options": "Plus d’options",
+ "multiple_accounts": "Plusieurs comptes",
"multiple_addresses_detected": "Plusieurs adresses détectées",
"mweb_ad": "Activez MWEB, la couche de confidentialité de Litecoin qui ajoute une anonymisation supplémentaire à vos coins",
"mweb_confirmed": "MWEB confirmé",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "Tous les modèles les plus récents (GPT-5.3, Claude 4.6).\nPas d'abonnement, payez en crypto.",
"narrow": "Étroit",
"network": "Réseau",
- "networks": "Réseaux",
"network_address_detected": "Adresse ${network} détectée",
"network_prefix_on": "sur",
"network_switch_failed": "Échec du basculement vers le réseau demandé. Le paiement n'a pas été appliqué, veuillez réessayer.",
@@ -1581,4 +1579,4 @@
"zcash_network_upgrade_desc_1": "Zcash a fait l'objet d'une mise à niveau du réseau appelée Ironwood qui nécessite la migration des fonds avant de pouvoir les dépenser. Cette migration est automatique, préserve la confidentialité et démarre dès que vous ouvrez votre portefeuille Zcash.",
"zcash_network_upgrade_desc_2": "Une fois la migration terminée, votre solde disponible reviendra à la normale et vous pourrez continuer à envoyer et recevoir du Zcash comme d'habitude.",
"zcash_network_upgrade_desc_3": "Veuillez laisser votre portefeuille Zcash terminer la synchronisation, puis laisser l'application ouverte jusqu'à la fin de la migration, ce qui peut prendre quelques minutes en fonction du solde."
-}
+}
\ No newline at end of file
### res/values/strings_gn.arb
@@ -238,8 +238,6 @@
"color_theme": "Téma sa'y",
"coming_soon_tag": "Og̃uahẽ pototáma",
"commit_transaction_amount_fee": "Emoneĩ ñembohasa\nMboheta: ${amount}\nHepyme’ẽ: ${fee}",
- "compatible_services": "Servicio-kuéra Ojokupytýva",
- "compatible_services_desc": "Eipuru pe Scan tembiporu emoñe'ẽ hag̃ua ko'ã QR código Cake Wallet oipytyvõva:",
"compatible_networks": "Redes Ojogueraháva",
"compatible_services": "Servicio-kuéra Ojokupytýva",
"compatible_services_desc": "Eipuru pe Scan tembiporu emoñe'ẽ hag̃ua ko'ã QR código Cake Wallet oipytyvõva:",
@@ -282,7 +280,6 @@
"contact_support": "Eñe’ẽ Support ndive",
"contacts": "Contactos",
"continue_text": "Eku'e",
- "contract_address": "Contrato Kundaharape",
"continue_to_swap": "Esegíkena eintercambia",
"contract_address": "Contrato Kundaharape",
"contract_warning": "Ko contrato dirección ojehechakuaa hag̃ua ikatuha haʼe peteĩ ñembotavy térã mba’e vai. Eñangareko porãke.",
@@ -478,7 +475,6 @@
"etherscan_history": "Etherscan rembiasakue",
"event": "Tembiaporã",
"events": "Tembiaporã",
- "evm_ecosystem_description": "Tekotevẽramo, emoambue pe red ha token reipuruséva ko jehepyme'ẽme g̃uarã.",
"evm_address_detected": "EVM Dirección Ojejuhúva",
"evm_ecosystem_description": "Tekotevẽramo, emoambue pe red ha token reipuruséva ko jehepyme'ẽme g̃uarã.",
"exchange": "Mbojopyru",
@@ -703,6 +699,7 @@
"moralis_nft_error": "Oiko peteĩ jejavy oñembyaty aja umi NFT. Ehecha porãke ne ñanduti joaju ha eha'ã jey.",
"more_details": "Detallesve",
"more_options": "Hetave jeporavorã",
+ "multiple_accounts": "Heta Cuenta rehegua",
"multiple_addresses_detected": "Heta kundaharape ojejuhu",
"mweb_ad": "Emboguata MWEB, pe capa ñemigua Litecoin-pe g̃uarã omoĩva anonimato extra nde viru'ikuérape",
"mweb_confirmed": "MWEB Oñemoañetéva",
@@ -717,7 +714,6 @@
"nanogpt_subtitle": "Opaite umi modelo ipyahuvéva (GPT-5.3, Claude 4.6).\nNdaipóri suscripción, ehepyme’ẽ cripto rupive.",
"narrow": "Poʼi",
"network": "Red",
- "networks": "Red-kuéra",
"network_address_detected": "${network} Dirección Ojejuhúva",
"network_prefix_on": "pe",
"network_switch_failed": "Ndojehechái oñembohasa hag̃ua red ojejerurévape. Pe pago ndojejapóikuri, eñeha’ã jey.",
@@ -1241,8 +1237,6 @@
"support_title_other_links": "Ambue juaju pytyvõ rehegua",
"supported": "Ojepytyvõva",
"swap": "Swap",
- "supported": "Ojepytyvõva",
- "swap": "Swap",
"swap_from_network": "Embohasa ${network} guive.",
"swap_from_network_description": "Emondo hag̃ua viru ${targetNetwork} red-pe, tekotevẽta rembohasa ne ${currentNetwork} viru. ¿Reprocedese piko?",
"swap_id": "ID ñemoambue rehegua",
@@ -1257,7 +1251,6 @@
"swipe_to_send": "Emoinge hag̃ua emondo hag̃ua.",
"switch_camera": "Ombohasa cámara",
"switch_input_currency": "Oñemoambue cripto ha fiat jeike apytépe",
- "switch_wallet": "Emoambue Billetera",
"switch_to_x_wallet": "Embohasa ${network} Wallet-pe",
"switch_wallet": "Emoambue Billetera",
"switchToETHWallet": "Ikatúpa emoambue peteĩ billetera Ethereum-pe ha eha’ãjey.",
@@ -1594,4 +1587,4 @@
"zcash_network_upgrade_desc_1": "Zcash ohasa peteî actualización red hérava Ironwood oikotevêva migración de fondos ikatu mboyve ojegasta. Ko migración ha’e automática, oñongatúva privacidad ha oñepyrũ eipe’a riremínte nde billetera Zcash.",
"zcash_network_upgrade_desc_2": "Opa vove pe migración, nde Saldo Disponible ohojeýta normal-pe, ha ikatu esegi emondo ha eguerahauka Zcash jepiveguáicha.",
"zcash_network_upgrade_desc_3": "Eheja ne billetera Zcash omohu’ã sincronización, ha upéi eheja pe app ojepe’a opa peve migración ikatúva ogueraha mbovymi minuto odependéva saldo rehe."
-}
+}
\ No newline at end of file
### res/values/strings_ha.arb
@@ -238,8 +238,6 @@
"color_theme": "Jigon launi",
"coming_soon_tag": "Zai zo nan ba da jimawa ba",
"commit_transaction_amount_fee": "Tabbatar da ma'amala\nAdadi: ${amount}\nKuɗi: ${fee}",
- "compatible_services": "Sabis masu jituwa",
- "compatible_services_desc": "Yi amfani da kayan aikin Scan don karanta nau'ikan QR code masu zuwa da Cake Wallet ke goyon baya:",
"compatible_networks": "Hanyoyin sadarwa masu jituwa",
"compatible_services": "Sabis masu jituwa",
"compatible_services_desc": "Yi amfani da kayan aikin Scan don karanta nau'ikan QR code masu zuwa da Cake Wallet ke goyon baya:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "Kuskure ya faru yayin da ake kawo NFTs. Da fatan za a duba haɗin intanet ɗinka kuma a sake gwadawa.",
"more_details": "Ƙarin Bayani",
"more_options": "Ƙarin Zaɓuɓɓuka",
+ "multiple_accounts": "Asusu da yawa",
"multiple_addresses_detected": "An gano adiresoshi da yawa",
"mweb_ad": "Kunna MWEB, layin sirri na Litecoin wanda ke ƙara ƙarin ɓoyewa ga tsabar kuɗin ku",
"mweb_confirmed": "MWEB da aka tabbatar",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "Duk sabbin samfura (GPT-5.3, Claude 4.6).\nBabu biyan kuɗin rajista, biya da crypto.",
"narrow": "Kunkuntar",
"network": "Hanyar sadarwa",
- "networks": "Hanyoyin sadarwa",
"network_address_detected": "An gano adireshin ${network}",
"network_prefix_on": "kan",
"network_switch_failed": "An kasa canzawa zuwa cibiyar sadarwar da ake nema. Ba a yi amfani da kuɗin ba, da fatan za a sake gwadawa.",
@@ -1584,4 +1582,4 @@
"zcash_network_upgrade_desc_1": "Zcash ya sami haɓaka hanyar sadarwa mai suna Ironwood wanda ke buƙatar ƙaura na kuɗi kafin a kashe su. Wannan ƙaura ta atomatik ce, tana kiyaye sirri, kuma tana farawa da zaran ka buɗe jakar kuɗin Zcash.",
"zcash_network_upgrade_desc_2": "Lokacin da ƙaura ta ƙare, Ma'aunin Ma'aunin ku na samuwa zai dawo daidai, kuma za ku iya ci gaba da aikawa da karɓar Zcash kamar yadda kuka saba.",
"zcash_network_upgrade_desc_3": "Da fatan za a ba da izinin walat ɗin ku na Zcash don gama daidaitawa, sannan ku bar app ɗin a buɗe har sai ƙaura ta ƙare wanda zai iya ɗaukar ƴan mintuna ya danganta da ma'auni."
-}
+}
\ No newline at end of file
### res/values/strings_hi.arb
@@ -238,8 +238,6 @@
"color_theme": "रंग थीम",
"coming_soon_tag": "जल्द आ रहा है",
"commit_transaction_amount_fee": "लेन-देन कमिट करें\nराशि: ${amount}\nशुल्क: ${fee}",
- "compatible_services": "संगत सेवाएँ",
- "compatible_services_desc": "Cake Wallet पर समर्थित निम्न प्रकार के QR कोड पढ़ने के लिए स्कैन टूल का उपयोग करें:",
"compatible_networks": "संगत नेटवर्क",
"compatible_services": "संगत सेवाएँ",
"compatible_services_desc": "Cake Wallet पर समर्थित निम्न प्रकार के QR कोड पढ़ने के लिए स्कैन टूल का उपयोग करें:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "NFTs प्राप्त करते समय एक त्रुटि हुई। कृपया अपना इंटरनेट कनेक्शन जाँचें और फिर से प्रयास करें।",
"more_details": "अधिक विवरण",
"more_options": "अधिक विकल्प",
+ "multiple_accounts": "एकाधिक खाते",
"multiple_addresses_detected": "कई पते पाए गए",
"mweb_ad": "MWEB सक्षम करें, Litecoin के लिए गोपनीयता लेयर जो आपके कॉइन्स में अतिरिक्त गुमनामी जोड़ती है",
"mweb_confirmed": "पुष्ट MWEB",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "सभी नवीनतम मॉडल (GPT-5.3, Claude 4.6)।\nकोई सब्सक्रिप्शन नहीं, क्रिप्टो से भुगतान करें।",
"narrow": "संकीर्ण",
"network": "नेटवर्क",
- "networks": "नेटवर्क",
"network_address_detected": "${network} पता मिला",
"network_prefix_on": "पर",
"network_switch_failed": "अनुरोधित नेटवर्क पर स्विच करने में विफल. भुगतान लागू नहीं हुआ, कृपया पुनः प्रयास करें।",
@@ -1583,4 +1581,4 @@
"zcash_network_upgrade_desc_1": "ज़कैश ने आयरनवुड नामक एक नेटवर्क अपग्रेड किया है जिसके लिए धन को खर्च करने से पहले स्थानांतरण की आवश्यकता होती है। यह माइग्रेशन स्वचालित, गोपनीयता-संरक्षित है, और जैसे ही आप अपना Zcash वॉलेट खोलते हैं, शुरू हो जाता है।",
"zcash_network_upgrade_desc_2": "जब माइग्रेशन समाप्त हो जाएगा, तो आपकी उपलब्ध शेष राशि सामान्य हो जाएगी, और आप हमेशा की तरह Zcash भेजना और प्राप्त करना जारी रख सकते हैं।",
"zcash_network_upgrade_desc_3": "कृपया अपने Zcash वॉलेट को सिंकिंग समाप्त करने की अनुमति दें, और फिर माइग्रेशन समाप्त होने तक ऐप को खुला छोड़ दें, जिसमें शेष राशि के आधार पर कुछ मिनट लग सकते हैं।"
-}
+}
\ No newline at end of file
### res/values/strings_hr.arb
@@ -238,8 +238,6 @@
"color_theme": "Tema boja",
"coming_soon_tag": "Uskoro",
"commit_transaction_amount_fee": "Potvrdi transakciju\nIznos: ${amount}\nNaknada: ${fee}",
- "compatible_services": "Kompatibilne usluge",
- "compatible_services_desc": "Upotrijebite alat Skeniraj za čitanje sljedećih vrsta QR kodova podržanih u Cake Walletu:",
"compatible_networks": "Kompatibilne mreže",
"compatible_services": "Kompatibilne usluge",
"compatible_services_desc": "Upotrijebite alat Skeniraj za čitanje sljedećih vrsta QR kodova podržanih u Cake Walletu:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "Došlo je do pogreške prilikom dohvaćanja NFT-ova. Provjerite internetsku vezu i pokušajte ponovno.",
"more_details": "Više detalja",
"more_options": "Više opcija",
+ "multiple_accounts": "Višestruki računi",
"multiple_addresses_detected": "Otkriveno je više adresa",
"mweb_ad": "Omogućite MWEB, sloj privatnosti za Litecoin koji vašim kovanicama dodaje dodatnu anonimnost",
"mweb_confirmed": "Potvrđeni MWEB",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "Svi najnoviji modeli (GPT-5.3, Claude 4.6).\nBez pretplate, plaćajte kriptom.",
"narrow": "Usko",
"network": "Mreža",
- "networks": "Mreže",
"network_address_detected": "Otkrivena ${network} adresa",
"network_prefix_on": "na",
"network_switch_failed": "Nije uspjelo prebacivanje na traženu mrežu. Plaćanje nije primijenjeno, pokušajte ponovno.",
@@ -1581,4 +1579,4 @@
"zcash_network_upgrade_desc_1": "Zcash je prošao mrežnu nadogradnju pod nazivom Ironwood koja zahtijeva migraciju sredstava prije nego što se mogu potrošiti. Ova migracija je automatska, čuva privatnost i počinje čim otvorite svoj Zcash novčanik.",
"zcash_network_upgrade_desc_2": "Kada migracija završi, vaš će se raspoloživi saldo vratiti u normalu i možete nastaviti slati i primati Zcash kao i obično.",
"zcash_network_upgrade_desc_3": "Dopustite svom Zcash novčaniku da dovrši sinkronizaciju, a zatim ostavite aplikaciju otvorenom dok migracija ne završi što može potrajati nekoliko minuta ovisno o stanju."
-}
+}
\ No newline at end of file
### res/values/strings_hy.arb
@@ -238,8 +238,6 @@
"color_theme": "Գունային թեմա",
"coming_soon_tag": "Շուտով",
"commit_transaction_amount_fee": "Հաստատել գործարքը\nԳումար՝ ${amount}\nՄիջնորդավճար՝ ${fee}",
- "compatible_services": "Համատեղելի ծառայություններ",
- "compatible_services_desc": "Օգտագործեք Սկանավորման գործիքը՝ Cake Wallet-ի կողմից աջակցվող QR կոդերի հետևյալ տեսակները կարդալու համար՝",
"compatible_networks": "Համատեղելի ցանցեր",
"compatible_services": "Համատեղելի ծառայություններ",
"compatible_services_desc": "Օգտագործեք Սկանավորման գործիքը՝ Cake Wallet-ի կողմից աջակցվող QR կոդերի հետևյալ տեսակները կարդալու համար՝",
@@ -699,6 +697,7 @@
"moralis_nft_error": "NFT-ները բեռնելիս սխալ է տեղի ունեցել։ Խնդրում ենք ստուգել ձեր ինտերնետային կապը և կրկին փորձել։",
"more_details": "Ավելի մանրամասներ",
"more_options": "Լրացուցիչ տարբերակներ",
+ "multiple_accounts": "Բազմաթիվ հաշիվներ",
"multiple_addresses_detected": "Հայտնաբերվել են բազմաթիվ հասցեներ",
"mweb_ad": "Միացնել MWEB-ը՝ Litecoin-ի գաղտնիության շերտը, որը լրացուցիչ անանունություն է ապահովում ձեր մետաղադրամների համար",
"mweb_confirmed": "Հաստատված MWEB",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "Բոլոր ամենանոր մոդելները (GPT-5.3, Claude 4.6).\nԱռանց բաժանորդագրության՝ վճարեք կրիպտոարժույթով։",
"narrow": "Նեղ",
"network": "Ցանց",
- "networks": "Ցանցեր",
"network_address_detected": "Հայտնաբերվել է ${network} հասցե",
"network_prefix_on": "վրա",
"network_switch_failed": "Չհաջողվեց անցնել պահանջվող ցանցին: Վճարումը չի կիրառվել, խնդրում ենք կրկին փորձել:",
@@ -1581,4 +1579,4 @@
"zcash_network_upgrade_desc_1": "Zcash-ը ենթարկվել է ցանցի արդիականացման, որը կոչվում է Ironwood, որը պահանջում է միջոցների միգրացիա՝ նախքան դրանք ծախսելը: Այս միգրացիան ավտոմատ է, պահպանում է գաղտնիությունը և սկսվում է ձեր Zcash դրամապանակը բացելուց անմիջապես հետո:",
"zcash_network_upgrade_desc_2": "Երբ միգրացիան ավարտվի, ձեր հասանելի մնացորդը կվերադառնա նորմալ, և դուք կարող եք շարունակել ուղարկել և ստանալ Zcash, ինչպես միշտ:",
"zcash_network_upgrade_desc_3": "Խնդրում ենք թույլ տալ, որ ձեր Zcash դրամապանակն ավարտի համաժամացումը, այնուհետև թողեք հավելվածը բաց, մինչև ավարտվի միգրացիան, որը կարող է տևել մի քանի րոպե՝ կախված մնացորդից:"
-}
+}
\ No newline at end of file
### res/values/strings_id.arb
@@ -238,8 +238,6 @@
"color_theme": "Tema warna",
"coming_soon_tag": "Segera hadir",
"commit_transaction_amount_fee": "Konfirmasi transaksi\nJumlah: ${amount}\nBiaya: ${fee}",
- "compatible_services": "Layanan yang Kompatibel",
- "compatible_services_desc": "Gunakan alat Pindai untuk membaca jenis kode QR berikut yang didukung di Cake Wallet:",
"compatible_networks": "Jaringan yang Kompatibel",
"compatible_services": "Layanan yang Kompatibel",
"compatible_services_desc": "Gunakan alat Pindai untuk membaca jenis kode QR berikut yang didukung di Cake Wallet:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "Terjadi kesalahan saat mengambil NFT. Silakan periksa koneksi internet Anda dan coba lagi.",
"more_details": "Detail Selengkapnya",
"more_options": "Opsi Lainnya",
+ "multiple_accounts": "Banyak Akun",
"multiple_addresses_detected": "Beberapa alamat terdeteksi",
"mweb_ad": "Aktifkan MWEB, lapisan privasi untuk Litecoin yang menambahkan anonimitas ekstra pada koin Anda",
"mweb_confirmed": "MWEB Terkonfirmasi",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "Semua model terbaru (GPT-5.3, Claude 4.6).\nTanpa langganan, bayar dengan kripto.",
"narrow": "Sempit",
"network": "Jaringan",
- "networks": "Jaringan",
"network_address_detected": "Alamat ${network} terdeteksi",
"network_prefix_on": "pada",
"network_switch_failed": "Gagal beralih ke jaringan yang diminta. Pembayaran tidak diterapkan, silakan coba lagi.",
@@ -1584,4 +1582,4 @@
"zcash_network_upgrade_desc_1": "Zcash telah mengalami peningkatan jaringan yang disebut Ironwood yang memerlukan migrasi dana sebelum dapat dibelanjakan. Migrasi ini bersifat otomatis, menjaga privasi, dan dimulai segera setelah Anda membuka dompet Zcash Anda.",
"zcash_network_upgrade_desc_2": "Ketika migrasi selesai, Saldo Tersedia Anda akan kembali normal, dan Anda dapat terus mengirim dan menerima Zcash seperti biasa.",
"zcash_network_upgrade_desc_3": "Harap izinkan dompet Zcash Anda menyelesaikan sinkronisasi, lalu biarkan aplikasi terbuka hingga migrasi selesai yang dapat memakan waktu beberapa menit tergantung pada saldo."
-}
+}
\ No newline at end of file
### res/values/strings_it.arb
@@ -238,8 +238,6 @@
"color_theme": "Tema colore",
"coming_soon_tag": "Prossimamente",
"commit_transaction_amount_fee": "Conferma transazione\nImporto: ${amount}\nCommissione: ${fee}",
- "compatible_services": "Servizi compatibili",
- "compatible_services_desc": "Usa lo strumento Scansione per leggere i seguenti tipi di codici QR supportati su Cake Wallet:",
"compatible_networks": "Reti compatibili",
"compatible_services": "Servizi compatibili",
"compatible_services_desc": "Usa lo strumento Scansione per leggere i seguenti tipi di codici QR supportati su Cake Wallet:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "Si è verificato un errore durante il recupero degli NFT. Controlla la tua connessione Internet e riprova.",
"more_details": "Maggiori dettagli",
"more_options": "Altre opzioni",
+ "multiple_accounts": "Conti multipli",
"multiple_addresses_detected": "Rilevati più indirizzi",
"mweb_ad": "Abilita MWEB, il livello di privacy per Litecoin che aggiunge ulteriore anonimato alle tue monete",
"mweb_confirmed": "MWEB confermato",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "Tutti i modelli più recenti (GPT-5.3, Claude 4.6).\nNessun abbonamento, paga con cripto.",
"narrow": "Stretto",
"network": "Rete",
- "networks": "Reti",
"network_address_detected": "Indirizzo ${network} rilevato",
"network_prefix_on": "SU",
"network_switch_failed": "Impossibile passare alla rete richiesta. Il pagamento non è stato applicato, riprova.",
@@ -1584,4 +1582,4 @@
"zcash_network_upgrade_desc_1": "Zcash ha subito un aggiornamento della rete chiamato Ironwood che richiede la migrazione dei fondi prima che possano essere spesi. Questa migrazione è automatica, rispetta la privacy e inizia non appena apri il tuo portafoglio Zcash.",
"zcash_network_upgrade_desc_2": "Al termine della migrazione, il tuo saldo disponibile tornerà alla normalità e potrai continuare a inviare e ricevere Zcash come al solito.",
"zcash_network_upgrade_desc_3": "Consenti al tuo portafoglio Zcash di completare la sincronizzazione, quindi lascia l'app aperta fino al termine della migrazione, che può richiedere alcuni minuti a seconda del saldo."
-}
+}
\ No newline at end of file
### res/values/strings_ja.arb
@@ -238,8 +238,6 @@
"color_theme": "カラーテーマ",
"coming_soon_tag": "近日公開",
"commit_transaction_amount_fee": "トランザクションを確定\n金額: ${amount}\n手数料: ${fee}",
- "compatible_services": "対応サービス",
- "compatible_services_desc": "スキャンツールを使用して、Cake Walletで対応している以下の種類のQRコードを読み取れます:",
"compatible_networks": "互換性のあるネットワーク",
"compatible_services": "対応サービス",
"compatible_services_desc": "スキャンツールを使用して、Cake Walletで対応している以下の種類のQRコードを読み取れます:",
@@ -700,6 +698,7 @@
"moralis_nft_error": "NFTの取得中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。",
"more_details": "詳細を見る",
"more_options": "その他のオプション",
+ "multiple_accounts": "複数のアカウント",
"multiple_addresses_detected": "複数のアドレスが検出されました",
"mweb_ad": "Litecoin のプライバシーレイヤーである MWEB を有効にして、コインの匿名性をさらに高めます",
"mweb_confirmed": "確認済みMWEB",
@@ -714,7 +713,6 @@
"nanogpt_subtitle": "すべての最新モデル(GPT-5.3、Claude 4.6)。\nサブスクリプション不要、暗号資産で支払い。",
"narrow": "狭い",
"network": "ネットワーク",
- "networks": "ネットワーク",
"network_address_detected": "${network}アドレスが検出されました",
"network_prefix_on": "の上",
"network_switch_failed": "要求されたネットワークへの切り替えに失敗しました。支払いは適用されませんでした。もう一度お試しください。",
@@ -1582,4 +1580,4 @@
"zcash_network_upgrade_desc_1": "Zcash は、Ironwood と呼ばれるネットワーク アップグレードを受けており、資金を使用する前に資金の移行が必要です。この移行は自動的に行われ、プライバシーが保護され、Zcash ウォレットを開くとすぐに開始されます。",
"zcash_network_upgrade_desc_2": "移行が完了すると、利用可能残高は通常の状態に戻り、引き続き通常どおり Zcash を送受信できます。",
"zcash_network_upgrade_desc_3": "Zcash ウォレットの同期が完了するまで待ってから、移行が完了するまでアプリを開いたままにしてください。残高によっては数分かかる場合があります。"
-}
+}
\ No newline at end of file
### res/values/strings_ko.arb
@@ -238,8 +238,6 @@
"color_theme": "색상 테마",
"coming_soon_tag": "출시 예정",
"commit_transaction_amount_fee": "트랜잭션 제출\n금액: ${amount}\n수수료: ${fee}",
- "compatible_services": "호환 서비스",
- "compatible_services_desc": "스캔 도구를 사용하여 Cake Wallet에서 지원하는 다음 유형의 QR 코드를 읽으세요:",
"compatible_networks": "호환 가능한 네트워크",
"compatible_services": "호환 서비스",
"compatible_services_desc": "스캔 도구를 사용하여 Cake Wallet에서 지원하는 다음 유형의 QR 코드를 읽으세요:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "NFT를 가져오는 중 오류가 발생했습니다. 인터넷 연결을 확인한 후 다시 시도해 주세요.",
"more_details": "자세히 보기",
"more_options": "추가 옵션",
+ "multiple_accounts": "여러 계정",
"multiple_addresses_detected": "여러 주소가 감지되었습니다",
"mweb_ad": "코인에 추가적인 익명성을 더해주는 Litecoin의 프라이버시 레이어인 MWEB를 활성화하세요",
"mweb_confirmed": "확인된 MWEB",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "최신 모델 모두(GPT-5.3, Claude 4.6).\n구독 없이 암호화폐로 결제.",
"narrow": "좁게",
"network": "네트워크",
- "networks": "네트워크",
"network_address_detected": "${network} 주소가 감지되었습니다",
"network_prefix_on": "~에",
"network_switch_failed": "요청한 네트워크로 전환하지 못했습니다. 결제가 적용되지 않았습니다. 다시 시도해 주세요.",
@@ -1581,4 +1579,4 @@
"zcash_network_upgrade_desc_1": "Zcash는 자금을 사용하기 전에 자금을 이전해야 하는 Ironwood라는 네트워크 업그레이드를 거쳤습니다. 이 마이그레이션은 자동으로 이루어지며 개인 정보가 보호되며 Zcash 지갑을 열자마자 시작됩니다.",
"zcash_network_upgrade_desc_2": "마이그레이션이 완료되면 사용 가능한 잔액이 정상으로 돌아가고 평소처럼 Zcash를 계속 보내고 받을 수 있습니다.",
"zcash_network_upgrade_desc_3": "Zcash 지갑 동기화가 완료될 때까지 기다린 후 마이그레이션이 완료될 때까지 앱을 열어두세요. 마이그레이션은 잔액에 따라 몇 분 정도 걸릴 수 있습니다."
-}
+}
\ No newline at end of file
### res/values/strings_my.arb
@@ -238,8 +238,6 @@
"color_theme": "အရောင်အပြင်အဆင်",
"coming_soon_tag": "မကြာမီလာမည်",
"commit_transaction_amount_fee": "ငွေလွှဲမှုကို အတည်ပြုပါ\nပမာဏ: ${amount}\nအခကြေးငွေ: ${fee}",
- "compatible_services": "တွဲဖက်အသုံးပြုနိုင်သော ဝန်ဆောင်မှုများ",
- "compatible_services_desc": "Cake Wallet တွင် ပံ့ပိုးထားသော အောက်ပါ QR ကုဒ်အမျိုးအစားများကို ဖတ်ရန် Scan tool ကို အသုံးပြုပါ:",
"compatible_networks": "လိုက်ဖက်သောကွန်ရက်များ",
"compatible_services": "တွဲဖက်အသုံးပြုနိုင်သော ဝန်ဆောင်မှုများ",
"compatible_services_desc": "Cake Wallet တွင် ပံ့ပိုးထားသော အောက်ပါ QR ကုဒ်အမျိုးအစားများကို ဖတ်ရန် Scan tool ကို အသုံးပြုပါ:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "NFTs ရယူနေစဉ် အမှားတစ်ခု ဖြစ်ပွားခဲ့သည်။ သင့်အင်တာနက်ချိတ်ဆက်မှုကို စစ်ဆေးပြီး ထပ်မံကြိုးစားပါ။",
"more_details": "အသေးစိတ်အချက်အလက်များ",
"more_options": "နောက်ထပ် ရွေးချယ်စရာများ",
+ "multiple_accounts": "အကောင့်များစွာ",
"multiple_addresses_detected": "လိပ်စာများစွာကို တွေ့ရှိခဲ့သည်",
"mweb_ad": "သင့်ဒင်္ဂါးများအတွက် အမည်ဝှက်မှုကို ပိုမိုမြှင့်တင်ပေးသည့် Litecoin ၏ သီးသန့်အလွှာ MWEB ကို ဖွင့်ပါ",
"mweb_confirmed": "အတည်ပြုပြီးသော MWEB",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "နောက်ဆုံးပေါ်မော်ဒယ်များအားလုံး (GPT-5.3, Claude 4.6)။\nစာရင်းသွင်းမှုမလို၊ crypto ဖြင့်ပေးချေပါ။",
"narrow": "ကျဉ်း",
"network": "ကွန်ရက်",
- "networks": "ကွန်ရက်များ",
"network_address_detected": "${network} လိပ်စာကို တွေ့ရှိသည်",
"network_prefix_on": "on",
"network_switch_failed": "တောင်းဆိုထားသောကွန်ရက်သို့ ပြောင်း၍မရပါ။ ငွေပေးချေမှုကို မသက်ရောက်ပါ၊ ထပ်စမ်းကြည့်ပါ။",
@@ -1581,4 +1579,4 @@
"zcash_network_upgrade_desc_1": "Zcash သည် ၎င်းတို့ကို အသုံးမချမီ ရန်ပုံငွေများ ရွှေ့ပြောင်းရန် လိုအပ်သော Ironwood ဟုခေါ်သော ကွန်ရက်အဆင့်မြှင့်တင်မှုကို လုပ်ဆောင်ခဲ့သည်။ ဤရွေ့ပြောင်းမှုသည် အလိုအလျောက်ဖြစ်ပြီး၊ လျှို့ဝှက်ရေးထိန်းသိမ်းမှုဖြစ်ပြီး၊ သင်၏ Zcash ပိုက်ဆံအိတ်ကို သင်ဖွင့်ပြီးသည်နှင့် စတင်သည်။",
"zcash_network_upgrade_desc_2": "ပြောင်းရွှေ့ခြင်းပြီးဆုံးသောအခါ၊ သင်၏ရရှိနိုင်သောလက်ကျန်ငွေသည် ပုံမှန်အတိုင်းပြန်လည်ရောက်ရှိမည်ဖြစ်ပြီး ပုံမှန်အတိုင်း Zcash ကို ဆက်လက်ပေးပို့နိုင်ပြီး လက်ခံနိုင်ပါသည်။",
"zcash_network_upgrade_desc_3": "သင်၏ Zcash ပိုက်ဆံအိတ်ကို စင့်ခ်လုပ်ခြင်းကို အပြီးသတ်ရန် ခွင့်ပြုပါ၊ ထို့နောက် လက်ကျန်ငွေပေါ်မူတည်၍ မိနစ်အနည်းငယ်ကြာနိုင်ပြီး ပြောင်းရွှေ့မှုပြီးဆုံးသည်အထိ အက်ပ်ကိုဖွင့်ထားလိုက်ပါ။"
-}
+}
\ No newline at end of file
### res/values/strings_nl.arb
@@ -238,8 +238,6 @@
"color_theme": "Kleurthema",
"coming_soon_tag": "Binnenkort",
"commit_transaction_amount_fee": "Bevestig transactie\nBedrag: ${amount}\nKosten: ${fee}",
- "compatible_services": "Compatibele services",
- "compatible_services_desc": "Gebruik de Scan-tool om de volgende soorten QR-codes te lezen die door Cake Wallet worden ondersteund:",
"compatible_networks": "Compatibele netwerken",
"compatible_services": "Compatibele services",
"compatible_services_desc": "Gebruik de Scan-tool om de volgende soorten QR-codes te lezen die door Cake Wallet worden ondersteund:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "Er is een fout opgetreden bij het ophalen van NFT's. Controleer je internetverbinding en probeer het opnieuw.",
"more_details": "Meer details",
"more_options": "Meer opties",
+ "multiple_accounts": "Meerdere accounts",
"multiple_addresses_detected": "Meerdere adressen gedetecteerd",
"mweb_ad": "Schakel MWEB in, de privacylaag voor Litecoin die extra anonimiteit aan je fondsen toevoegt",
"mweb_confirmed": "Bevestigde MWEB",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "Alle nieuwste modellen (GPT-5.3, Claude 4.6).\nGeen abonnement, betaal met crypto.",
"narrow": "Smal",
"network": "Netwerk",
- "networks": "Netwerken",
"network_address_detected": "${network}-adres gedetecteerd",
"network_prefix_on": "op",
"network_switch_failed": "Kan niet overschakelen naar het gevraagde netwerk. De betaling is niet toegepast. Probeer het opnieuw.",
@@ -1582,4 +1580,4 @@
"zcash_network_upgrade_desc_1": "Zcash heeft een netwerkupgrade ondergaan genaamd Ironwood, die migratie van fondsen vereist voordat ze kunnen worden uitgegeven. Deze migratie is automatisch, behoud van de privacy en begint zodra u uw Zcash-portemonnee opent.",
"zcash_network_upgrade_desc_2": "Wanneer de migratie is voltooid, wordt uw beschikbare saldo weer normaal en kunt u zoals gewoonlijk Zcash blijven verzenden en ontvangen.",
"zcash_network_upgrade_desc_3": "Wacht tot de synchronisatie van uw Zcash-portemonnee is voltooid en laat de app open totdat de migratie is voltooid. Dit kan enkele minuten duren, afhankelijk van het saldo."
-}
+}
\ No newline at end of file
### res/values/strings_pl.arb
@@ -238,8 +238,6 @@
"color_theme": "Motyw kolorystyczny",
"coming_soon_tag": "Wkrótce",
"commit_transaction_amount_fee": "Zatwierdź transakcję\nKwota: ${amount}\nOpłata: ${fee}",
- "compatible_services": "Zgodne usługi",
- "compatible_services_desc": "Użyj narzędzia Skanuj, aby odczytać następujące typy kodów QR obsługiwane w Cake Wallet:",
"compatible_networks": "Kompatybilne sieci",
"compatible_services": "Zgodne usługi",
"compatible_services_desc": "Użyj narzędzia Skanuj, aby odczytać następujące typy kodów QR obsługiwane w Cake Wallet:",
@@ -475,7 +473,6 @@
"etherscan_history": "Historia w Etherscan",
"event": "Zdarzenie",
"events": "Zdarzenia",
- "evm_ecosystem_description": "W razie potrzeby zmień sieć i token, których chcesz użyć do tej płatności.",
"evm_address_detected": "Wykryto adres EVM",
"evm_ecosystem_description": "W razie potrzeby zmień sieć i token, których chcesz użyć do tej płatności.",
"exchange": "Wymiana",
@@ -700,6 +697,7 @@
"moralis_nft_error": "Wystąpił błąd podczas pobierania NFT. Sprawdź połączenie internetowe i spróbuj ponownie.",
"more_details": "Więcej szczegółów",
"more_options": "Więcej opcji",
+ "multiple_accounts": "Wiele kont",
"multiple_addresses_detected": "Wykryto wiele adresów",
"mweb_ad": "Włącz MWEB, warstwę prywatności dla Litecoina, która zapewnia dodatkową anonimowość Twoim monetom",
"mweb_confirmed": "Potwierdzone MWEB",
@@ -714,7 +712,6 @@
"nanogpt_subtitle": "Wszystkie najnowsze modele (GPT-5.3, Claude 4.6).\nBez subskrypcji, płać kryptowalutą.",
"narrow": "Wąski",
"network": "Sieć",
- "networks": "Sieci",
"network_address_detected": "Wykryto adres ${network}",
"network_prefix_on": "NA",
"network_switch_failed": "Nie udało się przełączyć na żądaną sieć. Płatność nie została zastosowana. Spróbuj ponownie.",
@@ -1582,4 +1579,4 @@
"zcash_network_upgrade_desc_1": "Zcash przeszedł modernizację sieci o nazwie Ironwood, która wymaga migracji środków, zanim będzie można je wydać. Ta migracja jest automatyczna, chroni prywatność i rozpoczyna się natychmiast po otwarciu portfela Zcash.",
"zcash_network_upgrade_desc_2": "Po zakończeniu migracji Twoje dostępne saldo powróci do normy i będziesz mógł nadal wysyłać i odbierać Zcash jak zwykle.",
"zcash_network_upgrade_desc_3": "Poczekaj, aż Twój portfel Zcash dokończy synchronizację, a następnie pozostaw aplikację otwartą do czasu zakończenia migracji, co może zająć kilka minut, w zależności od salda."
-}
+}
\ No newline at end of file
### res/values/strings_pt.arb
@@ -238,8 +238,6 @@
"color_theme": "Tema de cores",
"coming_soon_tag": "Em breve",
"commit_transaction_amount_fee": "Confirmar transação\nValor: ${amount}\nTaxa: ${fee}",
- "compatible_services": "Serviços compatíveis",
- "compatible_services_desc": "Use a ferramenta de digitalização para ler os seguintes tipos de códigos QR suportados na Cake Wallet:",
"compatible_networks": "Redes Compatíveis",
"compatible_services": "Serviços compatíveis",
"compatible_services_desc": "Use a ferramenta de digitalização para ler os seguintes tipos de códigos QR suportados na Cake Wallet:",
@@ -700,6 +698,7 @@
"moralis_nft_error": "Ocorreu um erro ao buscar NFTs. Por favor, verifique sua conexão com a internet e tente novamente.",
"more_details": "Mais detalhes",
"more_options": "Mais opções",
+ "multiple_accounts": "Várias contas",
"multiple_addresses_detected": "Vários endereços detectados",
"mweb_ad": "Ative o MWEB, a camada de privacidade do Litecoin que adiciona anonimato extra às suas moedas",
"mweb_confirmed": "MWEB confirmado",
@@ -714,7 +713,6 @@
"nanogpt_subtitle": "Todos os modelos mais recentes (GPT-5.3, Claude 4.6).\nSem assinatura, pague com cripto.",
"narrow": "Estreito",
"network": "Rede",
- "networks": "Redes",
"network_address_detected": "Endereço ${network} detectado",
"network_prefix_on": "sobre",
"network_switch_failed": "Falha ao mudar para a rede solicitada. O pagamento não foi aplicado, tente novamente.",
@@ -1584,4 +1582,4 @@
"zcash_network_upgrade_desc_1": "Zcash passou por uma atualização de rede chamada Ironwood que requer migração de fundos antes que possam ser gastos. Esta migração é automática, preserva a privacidade e começa assim que você abre sua carteira Zcash.",
"zcash_network_upgrade_desc_2": "Quando a migração terminar, o seu Saldo Disponível voltará ao normal e você poderá continuar a enviar e receber Zcash normalmente.",
"zcash_network_upgrade_desc_3": "Aguarde a sincronização da sua carteira Zcash e deixe o aplicativo aberto até a migração terminar, o que pode levar alguns minutos dependendo do saldo."
-}
+}
\ No newline at end of file
### res/values/strings_ru.arb
@@ -238,8 +238,6 @@
"color_theme": "Цветовая тема",
"coming_soon_tag": "Скоро",
"commit_transaction_amount_fee": "Подтвердить транзакцию\nСумма: ${amount}\nКомиссия: ${fee}",
- "compatible_services": "Совместимые сервисы",
- "compatible_services_desc": "Используйте инструмент сканирования, чтобы считывать следующие типы QR-кодов, поддерживаемые в Cake Wallet:",
"compatible_networks": "Совместимые сети",
"compatible_services": "Совместимые сервисы",
"compatible_services_desc": "Используйте инструмент сканирования, чтобы считывать следующие типы QR-кодов, поддерживаемые в Cake Wallet:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "Произошла ошибка при получении NFT. Пожалуйста, проверьте подключение к интернету и попробуйте еще раз.",
"more_details": "Подробнее",
"more_options": "Дополнительные опции",
+ "multiple_accounts": "Несколько учетных записей",
"multiple_addresses_detected": "Обнаружено несколько адресов",
"mweb_ad": "Включите MWEB — уровень конфиденциальности Litecoin, который добавляет вашим монетам дополнительную анонимность.",
"mweb_confirmed": "Подтверждено MWEB",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "Все новейшие модели (GPT-5.3, Claude 4.6).\nБез подписки, оплата криптовалютой.",
"narrow": "Узкий",
"network": "Сеть",
- "networks": "Сети",
"network_address_detected": "Обнаружен адрес ${network}",
"network_prefix_on": "на",
"network_switch_failed": "Не удалось переключиться на запрошенную сеть. Платеж не был применен, попробуйте еще раз.",
@@ -1582,4 +1580,4 @@
"zcash_network_upgrade_desc_1": "Zcash претерпел обновление сети под названием Ironwood, которое требует миграции средств, прежде чем их можно будет потратить. Эта миграция является автоматической, сохраняет конфиденциальность и начинается, как только вы открываете свой кошелек Zcash.",
"zcash_network_upgrade_desc_2": "Когда миграция завершится, ваш доступный баланс вернется к нормальному состоянию, и вы сможете продолжать отправлять и получать Zcash в обычном режиме.",
"zcash_network_upgrade_desc_3": "Пожалуйста, дайте вашему кошельку Zcash завершить синхронизацию, а затем оставьте приложение открытым до завершения миграции, что может занять несколько минут в зависимости от баланса."
-}
+}
\ No newline at end of file
### res/values/strings_th.arb
@@ -238,8 +238,6 @@
"color_theme": "ธีมสี",
"coming_soon_tag": "เร็ว ๆ นี้",
"commit_transaction_amount_fee": "ยืนยันธุรกรรม\nจำนวน: ${amount}\nค่าธรรมเนียม: ${fee}",
- "compatible_services": "บริการที่รองรับ",
- "compatible_services_desc": "ใช้เครื่องมือสแกนเพื่ออ่าน QR Code ประเภทต่อไปนี้ที่ Cake Wallet รองรับ:",
"compatible_networks": "เครือข่ายที่เข้ากันได้",
"compatible_services": "บริการที่รองรับ",
"compatible_services_desc": "ใช้เครื่องมือสแกนเพื่ออ่าน QR Code ประเภทต่อไปนี้ที่ Cake Wallet รองรับ:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "เกิดข้อผิดพลาดขณะดึงข้อมูล NFT กรุณาตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณแล้วลองอีกครั้ง",
"more_details": "รายละเอียดเพิ่มเติม",
"more_options": "ตัวเลือกเพิ่มเติม",
+ "multiple_accounts": "หลายบัญชี",
"multiple_addresses_detected": "ตรวจพบที่อยู่หลายรายการ",
"mweb_ad": "เปิดใช้งาน MWEB เลเยอร์ความเป็นส่วนตัวสำหรับ Litecoin ที่เพิ่มความไม่เปิดเผยตัวตนให้กับเหรียญของคุณ",
"mweb_confirmed": "MWEB ที่ยืนยันแล้ว",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "โมเดลใหม่ล่าสุดทั้งหมด (GPT-5.3, Claude 4.6)\nไม่ต้องสมัครสมาชิก จ่ายด้วยคริปโต",
"narrow": "แคบ",
"network": "เครือข่าย",
- "networks": "เครือข่าย",
"network_address_detected": "ตรวจพบที่อยู่ ${network}",
"network_prefix_on": "บน",
"network_switch_failed": "ไม่สามารถสลับไปยังเครือข่ายที่ร้องขอได้ การชำระเงินไม่ได้ใช้ โปรดลองอีกครั้ง",
@@ -1581,4 +1579,4 @@
"zcash_network_upgrade_desc_1": "Zcash ได้รับการอัปเกรดเครือข่ายที่เรียกว่า Ironwood ซึ่งต้องมีการโยกย้ายเงินทุนก่อนจึงจะสามารถใช้งานได้ การโยกย้ายนี้เป็นไปโดยอัตโนมัติ รักษาความเป็นส่วนตัว และเริ่มต้นทันทีที่คุณเปิดกระเป๋าเงิน Zcash",
"zcash_network_upgrade_desc_2": "เมื่อการย้ายข้อมูลเสร็จสิ้น ยอดคงเหลือที่มีอยู่ของคุณจะกลับมาเป็นปกติ และคุณสามารถส่งและรับ Zcash ได้ตามปกติ",
"zcash_network_upgrade_desc_3": "โปรดอนุญาตให้กระเป๋าเงิน Zcash ของคุณทำการซิงค์ให้เสร็จสิ้น จากนั้นเปิดแอปทิ้งไว้จนกว่าการโยกย้ายจะเสร็จสิ้นซึ่งอาจใช้เวลาสักครู่ขึ้นอยู่กับยอดเงินคงเหลือ"
-}
+}
\ No newline at end of file
### res/values/strings_tl.arb
@@ -238,8 +238,6 @@
"color_theme": "Tema ng kulay",
"coming_soon_tag": "Paparating na",
"commit_transaction_amount_fee": "I-commit ang transaksyon\nHalaga: ${amount}\nBayad: ${fee}",
- "compatible_services": "Mga Katugmang Serbisyo",
- "compatible_services_desc": "Gamitin ang Scan tool upang basahin ang mga sumusunod na uri ng QR code na sinusuportahan sa Cake Wallet:",
"compatible_networks": "Mga Katugmang Network",
"compatible_services": "Mga Katugmang Serbisyo",
"compatible_services_desc": "Gamitin ang Scan tool upang basahin ang mga sumusunod na uri ng QR code na sinusuportahan sa Cake Wallet:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "May naganap na error habang kinukuha ang mga NFT. Pakisuri ang iyong koneksyon sa internet at subukang muli.",
"more_details": "Higit pang Detalye",
"more_options": "Higit pang mga opsyon",
+ "multiple_accounts": "Maramihang Mga Account",
"multiple_addresses_detected": "Maramihang address ang natukoy",
"mweb_ad": "I-enable ang MWEB, ang privacy layer para sa Litecoin na nagdaragdag ng karagdagang anonymity sa iyong mga coin",
"mweb_confirmed": "Nakumpirmang MWEB",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "Lahat ng pinakabagong modelo (GPT-5.3, Claude 4.6).\nWalang subscription, magbayad gamit ang crypto.",
"narrow": "Makitid",
"network": "Network",
- "networks": "Mga Network",
"network_address_detected": "Natukoy ang ${network} address",
"network_prefix_on": "sa",
"network_switch_failed": "Nabigong lumipat sa hiniling na network. Hindi nailapat ang pagbabayad, pakisubukang muli.",
@@ -1581,4 +1579,4 @@
"zcash_network_upgrade_desc_1": "Sumailalim ang Zcash sa isang network upgrade na tinatawag na Ironwood na nangangailangan ng paglipat ng mga pondo bago sila magastos. Ang paglipat na ito ay awtomatiko, pinapanatili ang privacy, at magsisimula sa sandaling mabuksan mo ang iyong Zcash wallet.",
"zcash_network_upgrade_desc_2": "Kapag natapos na ang paglipat, babalik sa normal ang iyong Available na Balanse, at maaari kang magpatuloy sa pagpapadala at pagtanggap ng Zcash gaya ng dati.",
"zcash_network_upgrade_desc_3": "Mangyaring payagan ang iyong Zcash wallet na matapos ang pag-sync, at pagkatapos ay iwanang bukas ang app hanggang sa matapos ang paglipat na maaaring tumagal ng ilang minuto depende sa balanse."
-}
+}
\ No newline at end of file
### res/values/strings_tr.arb
@@ -238,8 +238,6 @@
"color_theme": "Renk teması",
"coming_soon_tag": "Yakında",
"commit_transaction_amount_fee": "İşlemi onayla\nTutar: ${amount}\nÜcret: ${fee}",
- "compatible_services": "Uyumlu Hizmetler",
- "compatible_services_desc": "Cake Wallet'ta desteklenen aşağıdaki QR kodu türlerini okumak için Tarama aracını kullanın:",
"compatible_networks": "Uyumlu Ağlar",
"compatible_services": "Uyumlu Hizmetler",
"compatible_services_desc": "Cake Wallet'ta desteklenen aşağıdaki QR kodu türlerini okumak için Tarama aracını kullanın:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "NFT'ler alınırken bir hata oluştu. Lütfen internet bağlantınızı kontrol edin ve tekrar deneyin.",
"more_details": "Daha Fazla Ayrıntı",
"more_options": "Daha Fazla Seçenek",
+ "multiple_accounts": "Çoklu Hesaplar",
"multiple_addresses_detected": "Birden fazla adres algılandı",
"mweb_ad": "Coin’lerinize ekstra anonimlik ekleyen Litecoin gizlilik katmanı MWEB’i etkinleştirin",
"mweb_confirmed": "Onaylanmış MWEB",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "En yeni modellerin tümü (GPT-5.3, Claude 4.6).\nAbonelik yok, kripto ile ödeyin.",
"narrow": "Dar",
"network": "Ağ",
- "networks": "Ağlar",
"network_address_detected": "${network} adresi algılandı",
"network_prefix_on": "Açık",
"network_switch_failed": "İstenilen ağa geçiş yapılamadı. Ödeme uygulanmadı, lütfen tekrar deneyin.",
@@ -1581,4 +1579,4 @@
"zcash_network_upgrade_desc_1": "Zcash, Ironwood adında, fonların harcanmadan önce taşınmasını gerektiren bir ağ yükseltmesinden geçti. Bu geçiş otomatiktir, gizliliği korur ve Zcash cüzdanınızı açar açmaz başlar.",
"zcash_network_upgrade_desc_2": "Geçiş tamamlandığında Kullanılabilir Bakiyeniz normale dönecek ve her zamanki gibi Zcash gönderip almaya devam edebileceksiniz.",
"zcash_network_upgrade_desc_3": "Lütfen Zcash cüzdanınızın senkronizasyonu bitirmesine izin verin ve ardından bakiyeye bağlı olarak birkaç dakika sürebilecek taşıma işlemi bitene kadar uygulamayı açık bırakın."
-}
+}
\ No newline at end of file
### res/values/strings_uk.arb
@@ -238,8 +238,6 @@
"color_theme": "Колірна тема",
"coming_soon_tag": "Скоро",
"commit_transaction_amount_fee": "Підтвердити транзакцію\nСума: ${amount}\nКомісія: ${fee}",
- "compatible_services": "Сумісні сервіси",
- "compatible_services_desc": "Скористайтеся інструментом сканування, щоб зчитати такі типи QR-кодів, які підтримує Cake Wallet:",
"compatible_networks": "Сумісні мережі",
"compatible_services": "Сумісні сервіси",
"compatible_services_desc": "Скористайтеся інструментом сканування, щоб зчитати такі типи QR-кодів, які підтримує Cake Wallet:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "Під час отримання NFT сталася помилка. Будь ласка, перевірте підключення до Інтернету та спробуйте ще раз.",
"more_details": "Більше деталей",
"more_options": "Більше опцій",
+ "multiple_accounts": "Кілька облікових записів",
"multiple_addresses_detected": "Виявлено кілька адрес",
"mweb_ad": "Увімкніть MWEB — рівень конфіденційності для Litecoin, який додає додаткову анонімність вашим монетам",
"mweb_confirmed": "Підтверджено MWEB",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "Усі найновіші моделі (GPT-5.3, Claude 4.6).\nБез підписки, оплата криптовалютою.",
"narrow": "Вузький",
"network": "Мережа",
- "networks": "Мережі",
"network_address_detected": "Виявлено адресу ${network}",
"network_prefix_on": "на",
"network_switch_failed": "Не вдалося переключитися на потрібну мережу. Платіж не застосовано, спробуйте ще раз.",
@@ -1582,4 +1580,4 @@
"zcash_network_upgrade_desc_1": "Zcash пройшов оновлену мережу під назвою Ironwood, яка вимагає міграції коштів, перш ніж їх можна буде витратити. Ця міграція відбувається автоматично, зберігає конфіденційність і починається, щойно ви відкриваєте гаманець Zcash.",
"zcash_network_upgrade_desc_2": "Після завершення міграції ваш доступний баланс повернеться до нормального, і ви зможете продовжувати надсилати й отримувати Zcash, як зазвичай.",
"zcash_network_upgrade_desc_3": "Дочекайтеся завершення синхронізації вашого гаманця Zcash, а потім залиште програму відкритою, доки не завершиться міграція, яка може зайняти кілька хвилин залежно від балансу."
-}
+}
\ No newline at end of file
### res/values/strings_ur.arb
@@ -238,8 +238,6 @@
"color_theme": "رنگ تھیم",
"coming_soon_tag": "جلد آ رہا ہے",
"commit_transaction_amount_fee": "لین دین کی تصدیق کریں\nرقم: ${amount}\nفیس: ${fee}",
- "compatible_services": "موافق سروسز",
- "compatible_services_desc": "Cake Wallet پر معاون درج ذیل اقسام کے QR کوڈز پڑھنے کے لیے Scan ٹول استعمال کریں:",
"compatible_networks": "ہم آہنگ نیٹ ورکس",
"compatible_services": "موافق سروسز",
"compatible_services_desc": "Cake Wallet پر معاون درج ذیل اقسام کے QR کوڈز پڑھنے کے لیے Scan ٹول استعمال کریں:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "NFTs حاصل کرتے وقت ایک خرابی پیش آگئی۔ براہِ کرم اپنا انٹرنیٹ کنکشن چیک کریں اور دوبارہ کوشش کریں۔",
"more_details": "مزید تفصیلات",
"more_options": "مزید اختیارات",
+ "multiple_accounts": "متعدد اکاؤنٹس",
"multiple_addresses_detected": "متعدد پتے کا پتہ چلا",
"mweb_ad": "MWEB کو فعال کریں، Litecoin کے لیے رازداری کی پرت جو آپ کے کوائنز میں اضافی گمنامی شامل کرتی ہے۔",
"mweb_confirmed": "تصدیق شدہ MWEB",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "تمام جدید ترین ماڈلز (GPT-5.3، Claude 4.6)۔\nکوئی سبسکرپشن نہیں، کرپٹو کے ساتھ ادائیگی کریں۔",
"narrow": "تنگ",
"network": "نیٹ ورک",
- "networks": "نیٹ ورکس",
"network_address_detected": "${network} ایڈریس کا پتہ چلا",
"network_prefix_on": "پر",
"network_switch_failed": "درخواست کردہ نیٹ ورک پر سوئچ کرنے میں ناکام۔ ادائیگی لاگو نہیں ہوئی، براہ کرم دوبارہ کوشش کریں۔",
@@ -1583,4 +1581,4 @@
"zcash_network_upgrade_desc_1": "Zcash نے Ironwood نامی ایک نیٹ ورک اپ گریڈ کیا ہے جس کے لیے فنڈز خرچ کیے جانے سے پہلے ان کی منتقلی کی ضرورت ہوتی ہے۔ یہ منتقلی خودکار ہے، رازداری کو محفوظ رکھتی ہے، اور آپ کے Zcash والیٹ کو کھولتے ہی شروع ہو جاتی ہے۔",
"zcash_network_upgrade_desc_2": "منتقلی ختم ہونے پر، آپ کا دستیاب بیلنس معمول پر آجائے گا، اور آپ ہمیشہ کی طرح Zcash بھیجنا اور وصول کرنا جاری رکھ سکتے ہیں۔",
"zcash_network_upgrade_desc_3": "براہ کرم اپنے Zcash والیٹ کو مطابقت پذیری مکمل کرنے کی اجازت دیں، اور پھر ایپ کو مائیگریشن مکمل ہونے تک کھلا چھوڑ دیں جس میں بیلنس کے لحاظ سے چند منٹ لگ سکتے ہیں۔"
-}
+}
\ No newline at end of file
### res/values/strings_vi.arb
@@ -238,8 +238,6 @@
"color_theme": "Chủ đề màu",
"coming_soon_tag": "Sắp ra mắt",
"commit_transaction_amount_fee": "Xác nhận giao dịch\nSố tiền: ${amount}\nPhí: ${fee}",
- "compatible_services": "Dịch vụ tương thích",
- "compatible_services_desc": "Sử dụng công cụ Quét để đọc các loại mã QR sau được hỗ trợ trên Cake Wallet:",
"compatible_networks": "Mạng tương thích",
"compatible_services": "Dịch vụ tương thích",
"compatible_services_desc": "Sử dụng công cụ Quét để đọc các loại mã QR sau được hỗ trợ trên Cake Wallet:",
@@ -700,6 +698,7 @@
"moralis_nft_error": "Đã xảy ra lỗi khi tải NFT. Vui lòng kiểm tra kết nối Internet của bạn và thử lại.",
"more_details": "Xem thêm chi tiết",
"more_options": "Tùy chọn khác",
+ "multiple_accounts": "Nhiều tài khoản",
"multiple_addresses_detected": "Đã phát hiện nhiều địa chỉ",
"mweb_ad": "Bật MWEB, lớp quyền riêng tư cho Litecoin giúp tăng thêm tính ẩn danh cho coin của bạn",
"mweb_confirmed": "MWEB đã xác nhận",
@@ -714,7 +713,6 @@
"nanogpt_subtitle": "Tất cả các mô hình mới nhất (GPT-5.3, Claude 4.6).\nKhông cần đăng ký, thanh toán bằng crypto.",
"narrow": "Hẹp",
"network": "Mạng",
- "networks": "Mạng",
"network_address_detected": "Đã phát hiện địa chỉ ${network}",
"network_prefix_on": "TRÊN",
"network_switch_failed": "Không thể chuyển sang mạng được yêu cầu. Thanh toán chưa được áp dụng, vui lòng thử lại.",
@@ -1582,4 +1580,4 @@
"zcash_network_upgrade_desc_1": "Zcash đã trải qua quá trình nâng cấp mạng có tên Ironwood, yêu cầu di chuyển tiền trước khi chúng có thể được chi tiêu. Quá trình di chuyển này diễn ra tự động, bảo vệ quyền riêng tư và bắt đầu ngay khi bạn mở ví Zcash của mình.",
"zcash_network_upgrade_desc_2": "Khi quá trình di chuyển kết thúc, Số dư khả dụng của bạn sẽ trở lại bình thường và bạn có thể tiếp tục gửi và nhận Zcash như bình thường.",
"zcash_network_upgrade_desc_3": "Vui lòng cho phép ví Zcash của bạn hoàn tất đồng bộ hóa, sau đó để ứng dụng mở cho đến khi quá trình di chuyển hoàn tất, quá trình này có thể mất vài phút tùy thuộc vào số dư."
-}
+}
\ No newline at end of file
### res/values/strings_yo.arb
@@ -238,8 +238,6 @@
"color_theme": "Àkòrí àwọ̀",
"coming_soon_tag": "N bọ laipẹ",
"commit_transaction_amount_fee": "Fọwọ́sí ìdúnàdúrà\nIye: ${amount}\nOwó ìdúnàdúrà: ${fee}",
- "compatible_services": "Àwọn Iṣẹ́ Tó Bámu",
- "compatible_services_desc": "Lo irinṣẹ́ Scan láti ka àwọn irú kóòdù QR wọ̀nyí tí a ṣe àtìlẹ́yìn fún lórí Cake Wallet:",
"compatible_networks": "Awọn nẹtiwọki ibaramu",
"compatible_services": "Àwọn Iṣẹ́ Tó Bámu",
"compatible_services_desc": "Lo irinṣẹ́ Scan láti ka àwọn irú kóòdù QR wọ̀nyí tí a ṣe àtìlẹ́yìn fún lórí Cake Wallet:",
@@ -700,6 +698,7 @@
"moralis_nft_error": "Aṣiṣe kan ṣẹlẹ nígbà tí a ń gba àwọn NFT. Jọ̀wọ́ ṣàyẹ̀wò asopọ intanẹẹti rẹ, kí o sì tún gbìyànjú lẹ́ẹ̀kansi.",
"more_details": "Alaye diẹ sii",
"more_options": "Àwọn àṣàyàn míì",
+ "multiple_accounts": "Awọn iroyin pupọ",
"multiple_addresses_detected": "A ti rí ọ̀pọ̀ àdírẹ́sì",
"mweb_ad": "Mu MWEB ṣiṣẹ, ipele aṣiri fún Litecoin tó ń fi àìdánimọ kún àwọn owó rẹ",
"mweb_confirmed": "MWEB tí a ti jẹ́rìí sí",
@@ -714,7 +713,6 @@
"nanogpt_subtitle": "Gbogbo àwọn àwòṣe tuntun jùlọ (GPT-5.3, Claude 4.6).\nKò sí ìforúkọsílẹ̀, sanwó pẹ̀lú crypto.",
"narrow": "Dín",
"network": "Nẹ́tíwọ́ọ̀kì",
- "networks": "Àwọn Nẹ́tíwọ́ọ̀kì",
"network_address_detected": "A ti rí àdírẹ́sì ${network}",
"network_prefix_on": "lori",
"network_switch_failed": "Kuna lati yipada si nẹtiwọki ti o beere. Owo sisan ko lo, jọwọ gbiyanju lẹẹkansi.",
@@ -1582,4 +1580,4 @@
"zcash_network_upgrade_desc_1": "Zcash ti ṣe iṣagbega nẹtiwọọki kan ti a pe ni Ironwood ti o nilo ijira awọn owo ṣaaju ki wọn to le lo. Iṣilọ yii jẹ aifọwọyi, ipamọ-aṣiri, o si bẹrẹ ni kete ti o ṣii apamọwọ Zcash rẹ.",
"zcash_network_upgrade_desc_2": "Nigbati ijira ba pari, Iwontunws.funfun Wa rẹ yoo pada si deede, ati pe o le tẹsiwaju lati firanṣẹ ati gba Zcash bi igbagbogbo.",
"zcash_network_upgrade_desc_3": "Jọwọ gba apamọwọ Zcash rẹ lati pari mimuuṣiṣẹpọ, lẹhinna fi ohun elo naa silẹ ni sisi titi ti ijira yoo pari eyiti o le gba iṣẹju diẹ ti o da lori iwọntunwọnsi."
-}
+}
\ No newline at end of file
### res/values/strings_zh.arb
@@ -238,8 +238,6 @@
"color_theme": "颜色主题",
"coming_soon_tag": "即将推出",
"commit_transaction_amount_fee": "提交交易\n金额:${amount}\n手续费:${fee}",
- "compatible_services": "兼容的服务",
- "compatible_services_desc": "使用扫描工具读取 Cake Wallet 支持的以下类型二维码:",
"compatible_networks": "兼容网络",
"compatible_services": "兼容的服务",
"compatible_services_desc": "使用扫描工具读取 Cake Wallet 支持的以下类型二维码:",
@@ -699,6 +697,7 @@
"moralis_nft_error": "获取 NFT 时发生错误。请检查您的网络连接,然后重试。",
"more_details": "更多详情",
"more_options": "更多选项",
+ "multiple_accounts": "多个账户",
"multiple_addresses_detected": "检测到多个地址",
"mweb_ad": "启用 MWEB(莱特币的隐私层),为您的币添加额外的匿名性",
"mweb_confirmed": "已确认的 MWEB",
@@ -713,7 +712,6 @@
"nanogpt_subtitle": "所有最新模型(GPT-5.3、Claude 4.6)。\n无需订阅,用加密货币支付。",
"narrow": "窄",
"network": "网络",
- "networks": "网络",
"network_address_detected": "检测到 ${network} 地址",
"network_prefix_on": "在",
"network_switch_failed": "无法切换到请求的网络。付款未成功,请重试。",
@@ -1581,4 +1579,4 @@
"zcash_network_upgrade_desc_1": "Zcash 进行了名为 Ironwood 的网络升级,需要先迁移资金才能使用。此迁移是自动的、保护隐私的,并且在您打开 Zcash 钱包时立即开始。",
"zcash_network_upgrade_desc_2": "迁移完成后,您的可用余额将恢复正常,您可以继续照常发送和接收 Zcash。",
"zcash_network_upgrade_desc_3": "请让您的 Zcash 钱包完成同步,然后让应用程序保持打开状态,直到迁移完成,这可能需要几分钟的时间,具体取决于余额。"
-}
+}
\ No newline at end of file
### tool/configure.dart
@@ -103,6 +103,8 @@ import 'dart:typed_data';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cake_wallet/view_model/send/output.dart';
+import 'package:cw_bitcoin/electrum_balance.dart';
+import "package:cw_core/account.dart";
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/hardware/hardware_account_data.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
@@ -301,6 +303,11 @@ abstract class Bitcoin {
Future<String?> getLightningUsername(Object wallet);
Future<String?> getLightningInvoice(Object wallet, BigInt amount);
String? getBreezSdkError(Object exception);
+ Future<Account> getCurrentAccount(Object wallet);
+ ElectrumBalance balanceForAccount(Object wallet, int accountIndex);
+ Map<int, Object> accountBalancesSnapshot(Object wallet);
+ Future<void> setCurrentAccount(Object wallet, int accountIndex);
+ List<TransactionInfo> getCurrentAccountBitcoinTransactions(Object wallet);
}
""";
@@ -324,6 +331,7 @@ abstract class Bitcoin {
Future<void> generateMonero(bool hasImplementation) async {
final outputFile = File(moneroOutputPath);
const moneroCommonHeaders = """
+import 'package:cw_core/account.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/unspent_transaction_output.dart';
@@ -372,12 +380,6 @@ import 'package:cw_monero/pending_monero_transaction.dart';
""";
const moneroCwPart = "part 'cw_monero.dart';";
const moneroContent = """
-class Account {
- Account({required this.id, required this.label, this.balance});
- final int id;
- final String label;
- final String? balance;
-}
class Subaddress {
Subaddress({
@@ -536,6 +538,7 @@ abstract class MoneroAccountList {
Future<void> generateWownero(bool hasImplementation) async {
final outputFile = File(wowneroOutputPath);
const wowneroCommonHeaders = """
+import 'package:cw_core/account.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/unspent_transaction_output.dart';
@@ -585,12 +588,6 @@ import 'package:cw_wownero/pending_wownero_transaction.dart';
""";
const wowneroCwPart = "part 'cw_wownero.dart';";
const wowneroContent = """
-class Account {
- Account({required this.id, required this.label, this.balance});
- final int id;
- final String label;
- final String? balance;
-}
class Subaddress {
Subaddress({Why this scored 33/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.