Files
hash-wallet/cw_bitcoin/lib/bitcoin_address_record.dart
T
SerhiiandGitHub 6378e929b6 CW-1157-Use-correct-derivation-paths-for-other-address-types (#2825)
* feat: use bitcoin standard derivation paths per address type

* add support for Electrum derivation type in wallet

* fix merge conflict

* Use segwit HD for key export and simplify _hdFor

* skip derivation chooser for standard scan paths

* add legacy and P2SH derivation paths
2026-03-26 22:46:08 +01:00

204 lines
6.0 KiB
Dart

import 'dart:convert';
import 'package:cw_bitcoin/electrum_derivations.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:mobx/mobx.dart';
import 'package:bitcoin_base/bitcoin_base.dart';
abstract class BaseBitcoinAddressRecord {
BaseBitcoinAddressRecord(
this.address, {
required this.index,
this.isHidden = false,
this.isLegacyDerivation = false,
int txCount = 0,
int balance = 0,
String name = '',
bool isUsed = false,
required this.type,
required this.network,
}) : _txCount = txCount,
_balance = balance,
_name = name,
_isUsed = Observable(isUsed);
@override
bool operator ==(Object o) => o is BaseBitcoinAddressRecord && address == o.address;
final String address;
bool isHidden;
bool isLegacyDerivation;
final int index;
int _txCount;
int _balance;
String _name;
final Observable<bool> _isUsed;
BasedUtxoNetwork? network;
int get txCount => _txCount;
String get name => _name;
int get balance => _balance;
set txCount(int value) => _txCount = value;
set balance(int value) => _balance = value;
bool get isUsed => _isUsed.value;
void setAsUsed() => _isUsed.value = true;
void setNewName(String label) => _name = label;
int get hashCode => address.hashCode;
BitcoinAddressType type;
String toJSON();
}
class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
BitcoinAddressRecord(
super.address, {
required super.index,
super.isHidden = false,
super.isLegacyDerivation = false,
super.txCount = 0,
super.balance = 0,
super.name = '',
super.isUsed = false,
required super.type,
String? scriptHash,
required super.network,
}) {
try {
this.scriptHash = scriptHash ??
(network != null ? BitcoinAddressUtils.scriptHash(address, network: network!) : null);
} catch (e) {
printV(e);
}
}
static bool _legacyDefaultForType(BitcoinAddressType type) {
// Some address types (p2wpkh, p2wsh) were historically derived from the same account/path as our new standard
// but using legacy formats. For these, we default to legacy = false.
if (type == SegwitAddresType.p2wpkh || type == SegwitAddresType.p2wsh) return false;
// For other types (p2pkh, p2wpkh-in-p2sh, p2tr, etc.), old wallets used the non-standard
return true;
}
factory BitcoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) {
final decoded = json.decode(jsonSource) as Map;
final parsedType = decoded['type'] != null && decoded['type'] != ''
? BitcoinAddressType.values
.firstWhere((type) => type.toString() == decoded['type'] as String)
: SegwitAddresType.p2wpkh;
final parsedIsLegacy = decoded.containsKey('isLegacyDerivation')
? (decoded['isLegacyDerivation'] as bool? ?? false)
: _legacyDefaultForType(parsedType);
return BitcoinAddressRecord(
decoded['address'] as String,
index: decoded['index'] as int,
isHidden: decoded['isHidden'] as bool? ?? false,
isLegacyDerivation: parsedIsLegacy,
isUsed: decoded['isUsed'] as bool? ?? false,
txCount: decoded['txCount'] as int? ?? 0,
name: decoded['name'] as String? ?? '',
balance: decoded['balance'] as int? ?? 0,
type: parsedType,
scriptHash: decoded['scriptHash'] as String?,
network: network,
);
}
String? scriptHash;
String getScriptHash(BasedUtxoNetwork network) {
if (scriptHash != null) return scriptHash!;
try {
scriptHash = BitcoinAddressUtils.scriptHash(address, network: network);
} catch (e) {
return '';
}
return scriptHash!;
}
@override
String toJSON() => json.encode({
'address': address,
'index': index,
'isHidden': isHidden,
'isLegacyDerivation': isLegacyDerivation,
'isUsed': isUsed,
'txCount': txCount,
'name': name,
'balance': balance,
'type': type.toString(),
'scriptHash': scriptHash,
});
}
class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
BitcoinSilentPaymentAddressRecord(
super.address, {
required super.index,
super.isHidden = false,
super.txCount = 0,
super.balance = 0,
super.name = '',
super.isUsed = false,
required this.silentPaymentTweak,
required super.network,
required super.type,
this.spendDerivationPath = SILENT_PAYMENTS_SPEND_PATH_TESTNET,
});
factory BitcoinSilentPaymentAddressRecord.fromJSON(String jsonSource,
{BasedUtxoNetwork? network}) {
final decoded = json.decode(jsonSource) as Map;
return BitcoinSilentPaymentAddressRecord(
decoded['address'] as String,
index: decoded['index'] as int,
isHidden: decoded['isHidden'] as bool? ?? false,
isUsed: decoded['isUsed'] as bool? ?? false,
txCount: decoded['txCount'] as int? ?? 0,
name: decoded['name'] as String? ?? '',
balance: decoded['balance'] as int? ?? 0,
network: (decoded['network'] as String?) == null
? network
: BasedUtxoNetwork.fromName(decoded['network'] as String),
silentPaymentTweak: decoded['silent_payment_tweak'] as String?,
type: decoded['type'] != null && decoded['type'] != ''
? BitcoinAddressType.values
.firstWhere((type) => type.toString() == decoded['type'] as String)
: SilentPaymentsAddresType.p2sp,
spendDerivationPath:
decoded['spend_derivation_path'] as String? ?? SILENT_PAYMENTS_SPEND_PATH_TESTNET,
);
}
final String? silentPaymentTweak;
final String spendDerivationPath;
@override
String toJSON() => json.encode({
'address': address,
'index': index,
'isHidden': isHidden,
'isUsed': isUsed,
'txCount': txCount,
'name': name,
'balance': balance,
'type': type.toString(),
'network': network?.value,
'silent_payment_tweak': silentPaymentTweak,
'spend_derivation_path': spendDerivationPath,
});
}