dev
dart 246 lines 7.07 KB
Raw
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 }