Cw 1551 quick bitcoin wallet sync (#3446)

* add Wallet Accounts page and integrate UI

* account model refactor

* add walletInfoAccount table and API

* add WalletAccountList API

* add multi-account support for Electrum wallets

* add Bitcoin lightning card and card style handling

* refine account filtering and card selection

* update current account balance on wallet changes

* update configure.dar

* Use named parameters for card design

* Add Bitcoin account handling and refactor lookup

* refactor dashboard

* refactor cards UI and account balance handling

* Improve Bitcoin account selection and balances

* restrict unspent coin usage to current account

* filter transactions by current Bitcoin account

* refactor electrum wallet

* refactor address generation

* add accountIndex to address fetch logic

* Update configure.dart

* reload transactions on Bitcoin account change

* restore multiple BTC accounts and improve fetch

* show account balance with in btc

* track account index in Bitcoin transactions

* discover btc BIP39 accounts during restore

* fix account switching and card ordering bugs

* fix card customizer

* limit account name length in modal

* update BTC account list balance display

* auto-reformat

* formating fix

* skip HD map validation

* fix 0 balance issue

* minor fix

* quick bitcoin wallet sync

* restore shuffle change output

* restore address lookup fix

* Update electrum.dart

* remove account customizer

* Merge branch 'CW-1142-Add-accounts-feature-to-BTC' into CW-1551-Quick-Bitcoin-Wallet-Sync

* probe only segwit in account discovery

* add account discovery limit and tracking

* Improve Electrum sync account handling [skip ci]

* remove legacy monero account and address list UI

* rework BTC account balance refres

* avoid unnecessary BTC account reloads

* refactor account header in addresses page

* fix blank popup when editing an account

* fix: dedupe UTXO balance

* minor fix

* add Bitcoin current account API

* Apply batched suggestions from code review [skip ci]

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* address PR review comments [skip ci]

* remove account customizer and fix accounts init

* restrict extra BTC accounts to SegWit

* add BTC multi-account toggle

* Fix BTC sync gating and account balance update

* minor fix

* Update electrum_wallet.dart

* store selected account on WalletInfo

* Refine wallet account reset visibility

* refactor BTC address prep and move accounts page

* refactor account stack and customizer routing

* migrate legacy Lightning card styles

* unify dashboard account change state

* fix account routing and Electrum balances

* fix account setup and wallet cards UI

* disable Bitcoin Accounts for all hardware

* merge conflicts fix

* minor fixes

* update cards_view [skip ci]

* reverted cards view update [skip ci]

* fix asset name

* moved the transaction filtering into the electrum wallet

* localization

* disable accounts for watch-only wallets

* fix: stop card render from switching the wallet account

* fix: restore Lightning transactions in tx history

* minor fix

* fix: skip duplicate legacy address generation [skip ci]

* fix: correct account balance/design mismatch bugs

* addressing review comments

---------

Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
Serhii
2026-10-03 00:44:57 +02:00
committed by GitHub
co-authored by Omar Hatem Robert Malikowski
parent 0503d54290
commit d7ebf42814
84 changed files with 3991 additions and 2186 deletions
+10 -1
View File
@@ -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,
+29 -10
View File
@@ -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;
+9 -3
View File
@@ -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;
+30
View File
@@ -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,
);
}
@@ -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)';
}
}
File diff suppressed because it is too large Load Diff
+344 -113
View File
@@ -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);
for (final accountIndex in effectiveAccountIndexes) {
await _generateInitialAddresses(
type: P2shAddressType.p2wpkhInP2sh, isLegacyDerivation: true);
await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh);
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,15 +778,20 @@ 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);
}
@@ -694,6 +799,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
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,6 +814,7 @@ 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,
@@ -715,6 +822,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
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,6 +850,7 @@ 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,
@@ -748,6 +858,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
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;
}
+5 -2
View File
@@ -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),
@@ -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
@@ -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,