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 _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, }); }