| 1 | import 'dart:convert'; |
| 2 | import 'package:cw_bitcoin/electrum_derivations.dart'; |
| 3 | import 'package:cw_core/utils/print_verbose.dart'; |
| 4 | import 'package:mobx/mobx.dart'; |
| 5 | |
| 6 | import 'package:bitcoin_base/bitcoin_base.dart'; |
| 7 | |
| 8 | abstract class BaseBitcoinAddressRecord { |
| 9 | BaseBitcoinAddressRecord( |
| 10 | this.address, { |
| 11 | required this.index, |
| 12 | this.isHidden = false, |
| 13 | this.isLegacyDerivation = false, |
| 14 | int txCount = 0, |
| 15 | int balance = 0, |
| 16 | String name = '', |
| 17 | bool isUsed = false, |
| 18 | required this.type, |
| 19 | required this.network, |
| 20 | }) : _txCount = txCount, |
| 21 | _balance = balance, |
| 22 | _name = name, |
| 23 | _isUsed = Observable(isUsed); |
| 24 | |
| 25 | @override |
| 26 | bool operator ==(Object o) => o is BaseBitcoinAddressRecord && address == o.address; |
| 27 | |
| 28 | final String address; |
| 29 | bool isHidden; |
| 30 | bool isLegacyDerivation; |
| 31 | final int index; |
| 32 | int _txCount; |
| 33 | int _balance; |
| 34 | String _name; |
| 35 | final Observable<bool> _isUsed; |
| 36 | BasedUtxoNetwork? network; |
| 37 | |
| 38 | int get txCount => _txCount; |
| 39 | |
| 40 | String get name => _name; |
| 41 | |
| 42 | int get balance => _balance; |
| 43 | |
| 44 | set txCount(int value) => _txCount = value; |
| 45 | |
| 46 | set balance(int value) => _balance = value; |
| 47 | |
| 48 | bool get isUsed => _isUsed.value; |
| 49 | |
| 50 | void setAsUsed() => _isUsed.value = true; |
| 51 | void setNewName(String label) => _name = label; |
| 52 | |
| 53 | int get hashCode => address.hashCode; |
| 54 | |
| 55 | BitcoinAddressType type; |
| 56 | |
| 57 | String get derivationPath; |
| 58 | |
| 59 | String toJSON(); |
| 60 | } |
| 61 | |
| 62 | class BitcoinAddressRecord extends BaseBitcoinAddressRecord { |
| 63 | BitcoinAddressRecord( |
| 64 | super.address, { |
| 65 | required super.index, |
| 66 | super.isHidden = false, |
| 67 | super.isLegacyDerivation = false, |
| 68 | super.txCount = 0, |
| 69 | super.balance = 0, |
| 70 | super.name = '', |
| 71 | super.isUsed = false, |
| 72 | required super.type, |
| 73 | String? scriptHash, |
| 74 | required super.network, |
| 75 | }) { |
| 76 | try { |
| 77 | this.scriptHash = scriptHash ?? |
| 78 | (network != null ? BitcoinAddressUtils.scriptHash(address, network: network!) : null); |
| 79 | } catch (e) { |
| 80 | printV(e); |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | static bool _legacyDefaultForType(BitcoinAddressType type) { |
| 85 | // Some address types (p2wpkh, p2wsh) were historically derived from the same account/path as our new standard |
| 86 | // but using legacy formats. For these, we default to legacy = false. |
| 87 | if (type == SegwitAddresType.p2wpkh || type == SegwitAddresType.p2wsh) return false; |
| 88 | |
| 89 | // For other types (p2pkh, p2wpkh-in-p2sh, p2tr, etc.), old wallets used the non-standard |
| 90 | return true; |
| 91 | } |
| 92 | |
| 93 | factory BitcoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) { |
| 94 | final decoded = json.decode(jsonSource) as Map; |
| 95 | |
| 96 | final parsedType = decoded['type'] != null && decoded['type'] != '' |
| 97 | ? BitcoinAddressType.values |
| 98 | .firstWhere((type) => type.toString() == decoded['type'] as String) |
| 99 | : SegwitAddresType.p2wpkh; |
| 100 | |
| 101 | final parsedIsLegacy = decoded.containsKey('isLegacyDerivation') |
| 102 | ? (decoded['isLegacyDerivation'] as bool? ?? false) |
| 103 | : _legacyDefaultForType(parsedType); |
| 104 | |
| 105 | return BitcoinAddressRecord( |
| 106 | decoded['address'] as String, |
| 107 | index: decoded['index'] as int, |
| 108 | isHidden: decoded['isHidden'] as bool? ?? false, |
| 109 | isLegacyDerivation: parsedIsLegacy, |
| 110 | isUsed: decoded['isUsed'] as bool? ?? false, |
| 111 | txCount: decoded['txCount'] as int? ?? 0, |
| 112 | name: decoded['name'] as String? ?? '', |
| 113 | balance: decoded['balance'] as int? ?? 0, |
| 114 | type: parsedType, |
| 115 | scriptHash: decoded['scriptHash'] as String?, |
| 116 | network: network, |
| 117 | ); |
| 118 | } |
| 119 | |
| 120 | String? scriptHash; |
| 121 | |
| 122 | static int _purposeForType(BitcoinAddressType type) { |
| 123 | if (type == P2pkhAddressType.p2pkh) return 44; |
| 124 | if (type == P2shAddressType.p2wpkhInP2sh) return 49; |
| 125 | if (type == SegwitAddresType.p2wsh) return 48; |
| 126 | if (type == SegwitAddresType.p2tr) return 86; |
| 127 | return 84; |
| 128 | } |
| 129 | |
| 130 | int _coinTypeForNetwork() { |
| 131 | if (!(network?.isMainnet ?? true)) return 1; |
| 132 | |
| 133 | switch (network) { |
| 134 | case BitcoinNetwork.mainnet: |
| 135 | return 0; |
| 136 | case LitecoinNetwork.mainnet: |
| 137 | return 2; |
| 138 | case BitcoinCashNetwork.mainnet: |
| 139 | return 145; |
| 140 | case DogecoinNetwork.mainnet: |
| 141 | return 3; |
| 142 | default: |
| 143 | return 0; |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | String getScriptHash(BasedUtxoNetwork network) { |
| 148 | if (scriptHash != null) return scriptHash!; |
| 149 | try { |
| 150 | scriptHash = BitcoinAddressUtils.scriptHash(address, network: network); |
| 151 | } catch (e) { |
| 152 | return ''; |
| 153 | } |
| 154 | return scriptHash!; |
| 155 | } |
| 156 | |
| 157 | @override |
| 158 | String get derivationPath { |
| 159 | if (type == SegwitAddresType.mweb) { |
| 160 | return "m/1000'/$index"; |
| 161 | } |
| 162 | |
| 163 | final coinType = _coinTypeForNetwork(); |
| 164 | final purpose = _purposeForType(type); |
| 165 | final accountPath = isLegacyDerivation ? electrum_path : "m/$purpose'/$coinType'/0'"; |
| 166 | |
| 167 | final chain = isHidden ? 1 : 0; |
| 168 | return "$accountPath/$chain/$index"; |
| 169 | } |
| 170 | |
| 171 | @override |
| 172 | String toJSON() => json.encode({ |
| 173 | 'address': address, |
| 174 | 'index': index, |
| 175 | 'isHidden': isHidden, |
| 176 | 'isLegacyDerivation': isLegacyDerivation, |
| 177 | 'isUsed': isUsed, |
| 178 | 'txCount': txCount, |
| 179 | 'name': name, |
| 180 | 'balance': balance, |
| 181 | 'type': type.toString(), |
| 182 | 'scriptHash': scriptHash, |
| 183 | }); |
| 184 | } |
| 185 | |
| 186 | class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { |
| 187 | BitcoinSilentPaymentAddressRecord( |
| 188 | super.address, { |
| 189 | required super.index, |
| 190 | super.isHidden = false, |
| 191 | super.txCount = 0, |
| 192 | super.balance = 0, |
| 193 | super.name = '', |
| 194 | super.isUsed = false, |
| 195 | required this.silentPaymentTweak, |
| 196 | required super.network, |
| 197 | required super.type, |
| 198 | this.spendDerivationPath = SILENT_PAYMENTS_SPEND_PATH_TESTNET, |
| 199 | }); |
| 200 | |
| 201 | factory BitcoinSilentPaymentAddressRecord.fromJSON(String jsonSource, |
| 202 | {BasedUtxoNetwork? network}) { |
| 203 | final decoded = json.decode(jsonSource) as Map; |
| 204 | |
| 205 | return BitcoinSilentPaymentAddressRecord( |
| 206 | decoded['address'] as String, |
| 207 | index: decoded['index'] as int, |
| 208 | isHidden: decoded['isHidden'] as bool? ?? false, |
| 209 | isUsed: decoded['isUsed'] as bool? ?? false, |
| 210 | txCount: decoded['txCount'] as int? ?? 0, |
| 211 | name: decoded['name'] as String? ?? '', |
| 212 | balance: decoded['balance'] as int? ?? 0, |
| 213 | network: (decoded['network'] as String?) == null |
| 214 | ? network |
| 215 | : BasedUtxoNetwork.fromName(decoded['network'] as String), |
| 216 | silentPaymentTweak: decoded['silent_payment_tweak'] as String?, |
| 217 | type: decoded['type'] != null && decoded['type'] != '' |
| 218 | ? BitcoinAddressType.values |
| 219 | .firstWhere((type) => type.toString() == decoded['type'] as String) |
| 220 | : SilentPaymentsAddresType.p2sp, |
| 221 | spendDerivationPath: |
| 222 | decoded['spend_derivation_path'] as String? ?? SILENT_PAYMENTS_SPEND_PATH_TESTNET, |
| 223 | ); |
| 224 | } |
| 225 | |
| 226 | final String? silentPaymentTweak; |
| 227 | final String spendDerivationPath; |
| 228 | |
| 229 | @override |
| 230 | String get derivationPath => spendDerivationPath; |
| 231 | |
| 232 | @override |
| 233 | String toJSON() => json.encode({ |
| 234 | 'address': address, |
| 235 | 'index': index, |
| 236 | 'isHidden': isHidden, |
| 237 | 'isUsed': isUsed, |
| 238 | 'txCount': txCount, |
| 239 | 'name': name, |
| 240 | 'balance': balance, |
| 241 | 'type': type.toString(), |
| 242 | 'network': network?.value, |
| 243 | 'silent_payment_tweak': silentPaymentTweak, |
| 244 | 'spend_derivation_path': spendDerivationPath, |
| 245 | }); |
| 246 | } |