Files
hash-wallet/cw_bitcoin/lib/bitcoin_address_record.dart
547fae891a show Bitcoin address derivation path (#3177)
* show Bitcoin address derivation path

* Revert "show Bitcoin address derivation path"

* Update cw_bitcoin/lib/bitcoin_address_record.dart [skip ci]

* Update cw_bitcoin/lib/bitcoin_address_record.dart [skip ci]

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
2026-04-13 22:05:26 +02:00

249 lines
7.1 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 get derivationPath;
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;
static int _purposeForType(BitcoinAddressType type) {
if (type == P2pkhAddressType.p2pkh) return 44;
if (type == P2shAddressType.p2wpkhInP2sh) return 49;
if (type == SegwitAddresType.p2wsh) return 48;
if (type == SegwitAddresType.p2tr) return 86;
return 84;
}
int _coinTypeForNetwork() {
if (!(network?.isMainnet ?? true)) return 1;
switch (network) {
case BitcoinNetwork.mainnet:
return 0;
case LitecoinNetwork.mainnet:
return 2;
case BitcoinCashNetwork.mainnet:
return 145;
case DogecoinNetwork.mainnet:
return 3;
default:
return 0;
}
}
String getScriptHash(BasedUtxoNetwork network) {
if (scriptHash != null) return scriptHash!;
try {
scriptHash = BitcoinAddressUtils.scriptHash(address, network: network);
} catch (e) {
return '';
}
return scriptHash!;
}
@override
String get derivationPath {
if (type == SegwitAddresType.mweb) {
return "m/1000'/$index";
}
final coinType = _coinTypeForNetwork();
final purpose = _purposeForType(type);
final accountPath = isLegacyDerivation
? electrum_path
: "m/$purpose'/$coinType'/0'";
final chain = isHidden ? 1 : 0;
return "$accountPath/$chain/$index";
}
@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 get derivationPath => 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,
});
}