dev
dart 979 lines 32.6 KB
Raw
1 import 'dart:io' show Platform;
2 import 'dart:math';
3
4 import 'package:bitcoin_base/bitcoin_base.dart';
5 import 'package:blockchain_utils/blockchain_utils.dart';
6 import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart';
7 import 'package:cw_bitcoin/bitcoin_address_record.dart';
8 import 'package:cw_bitcoin/bitcoin_unspent.dart';
9 import 'package:cw_bitcoin/lightning/lightning_addres_type.dart';
10 import 'package:cw_bitcoin/lightning/lightning_wallet.dart';
11 import 'package:cw_core/pathForWallet.dart';
12 import 'package:cw_bitcoin/electrum_derivations.dart';
13 import 'package:cw_core/unspent_coin_type.dart';
14 import 'package:cw_core/utils/print_verbose.dart';
15 import 'package:cw_core/wallet_addresses.dart';
16 import 'package:cw_core/generate_name.dart';
17 import 'package:cw_core/wallet_info.dart';
18 import 'package:cw_core/wallet_type.dart';
19 import 'package:mobx/mobx.dart';
20
21 part 'electrum_wallet_addresses.g.dart';
22
23 abstract class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses;
24
25 const List<BitcoinAddressType> BITCOIN_ADDRESS_TYPES = [
26 SegwitAddresType.p2wpkh,
27 P2pkhAddressType.p2pkh,
28 SegwitAddresType.p2tr,
29 SegwitAddresType.p2wsh,
30 P2shAddressType.p2wpkhInP2sh,
31 ];
32
33 const List<BitcoinAddressType> LITECOIN_ADDRESS_TYPES = [
34 SegwitAddresType.p2wpkh,
35 SegwitAddresType.mweb,
36 ];
37
38 const List<BitcoinAddressType> BITCOIN_CASH_ADDRESS_TYPES = [
39 P2pkhAddressType.p2pkh,
40 ];
41
42 const List<BitcoinAddressType> DOGECOIN_ADDRESS_TYPES = [
43 P2pkhAddressType.p2pkh,
44 ];
45
46 abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
47 ElectrumWalletAddressesBase(
48 WalletInfo walletInfo, {
49 required this.mainHdByType,
50 required this.sideHdByType,
51 required this.legacyMainHd,
52 required this.legacySideHd,
53 required this.network,
54 required this.isHardwareWallet,
55 List<BitcoinAddressRecord>? initialAddresses,
56 Map<String, int>? initialRegularAddressIndex,
57 Map<String, int>? initialChangeAddressIndex,
58 List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
59 int initialSilentAddressIndex = 0,
60 List<BitcoinAddressRecord>? initialMwebAddresses,
61 Bip32Slip10Secp256k1? masterHd,
62 BitcoinAddressType? initialAddressPageType,
63 this.lightningWallet,
64 }) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
65 addressesByReceiveType =
66 ObservableList<BaseBitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()),
67 receiveAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
68 .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed)
69 .toSet()),
70 changeAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
71 .where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed)
72 .toSet()),
73 currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {},
74 currentChangeAddressIndexByType = initialChangeAddressIndex ?? {},
75 _addressPageType = initialAddressPageType ??
76 (walletInfo.addressPageType != null
77 ? walletInfo.addressPageType == LightningAddressType.p2l.value
78 ? LightningAddressType.p2l
79 : BitcoinAddressType.fromValue(walletInfo.addressPageType!)
80 : SegwitAddresType.p2wpkh),
81 silentAddresses = ObservableList<BitcoinSilentPaymentAddressRecord>.of(
82 (initialSilentAddresses ?? []).toSet()),
83 currentSilentAddressIndex = initialSilentAddressIndex,
84 mwebAddresses =
85 ObservableList<BitcoinAddressRecord>.of((initialMwebAddresses ?? []).toSet()),
86 lockedReceiveAddressByType = ObservableMap<BitcoinAddressType, String>(),
87 lightningAddress = lightningWallet?.cachedAddress,
88 super(walletInfo) {
89 if (masterHd != null) {
90 silentAddress = SilentPaymentOwner.fromPrivateKeys(
91 b_scan:
92 ECPrivate.fromHex(masterHd.derivePath(SILENT_PAYMENTS_SCAN_PATH).privateKey.toHex()),
93 b_spend:
94 ECPrivate.fromHex(masterHd.derivePath(SILENT_PAYMENTS_SPEND_PATH).privateKey.toHex()),
95 network: network,
96 );
97
98 // Clean the Silent Payment Addresses if the initial addresses are the old SP Addresses
99 if (!silentAddresses
100 .any((addr) => addr.index == 0 && addr.address == silentAddress.toString())) {
101 silentAddresses.clear();
102 }
103
104 if (!silentAddresses.any((addr) => addr.index == 0 && addr.isHidden == false))
105 silentAddresses.add(BitcoinSilentPaymentAddressRecord(
106 silentAddress.toString(),
107 index: 0,
108 isHidden: false,
109 name: "",
110 silentPaymentTweak: null,
111 network: network,
112 type: SilentPaymentsAddresType.p2sp,
113 ));
114 for (var i = 0; i < 5; i++) {
115 if (!silentAddresses.any((addr) => addr.index == i && addr.isHidden == (i == 0)))
116 silentAddresses.add(BitcoinSilentPaymentAddressRecord(
117 silentAddress!.toLabeledSilentPaymentAddress(i).toString(),
118 index: i,
119 isHidden: i == 0,
120 name: "",
121 silentPaymentTweak: BytesUtils.toHexString(silentAddress!.generateLabel(i)),
122 network: network,
123 type: SilentPaymentsAddresType.p2sp,
124 ));
125 }
126 }
127 updateAddressesByMatch();
128 }
129
130 static const defaultReceiveAddressesCount = 22;
131 static const defaultChangeAddressesCount = 17;
132 static const gap = 20;
133
134 final ObservableList<BitcoinAddressRecord> _addresses;
135 final ObservableList<BaseBitcoinAddressRecord> addressesByReceiveType;
136 final ObservableList<BitcoinAddressRecord> receiveAddresses;
137 final ObservableList<BitcoinAddressRecord> changeAddresses;
138
139 // TODO: add this variable in `bitcoin_wallet_addresses` and just add a cast in cw_bitcoin to use it
140 final ObservableList<BitcoinSilentPaymentAddressRecord> silentAddresses;
141
142 // TODO: add this variable in `litecoin_wallet_addresses` and just add a cast in cw_bitcoin to use it
143 final ObservableList<BitcoinAddressRecord> mwebAddresses;
144 final BasedUtxoNetwork network;
145 Map<BitcoinAddressType, Bip32Slip10Secp256k1> mainHdByType;
146 Map<BitcoinAddressType, Bip32Slip10Secp256k1> sideHdByType;
147 final Bip32Slip10Secp256k1 legacyMainHd;
148 final Bip32Slip10Secp256k1 legacySideHd;
149 final bool isHardwareWallet;
150 final LightningWallet? lightningWallet;
151
152 @observable
153 ObservableMap<BitcoinAddressType, String> lockedReceiveAddressByType;
154
155 @observable
156 SilentPaymentOwner? silentAddress;
157
158 @observable
159 late BitcoinAddressType _addressPageType;
160
161 @computed
162 BitcoinAddressType get addressPageType => _addressPageType;
163
164 @observable
165 String? activeSilentAddress;
166
167 @observable
168 String? lightningAddress;
169
170 @computed
171 List<BitcoinAddressRecord> get allAddresses => _addresses;
172
173 @observable
174 bool addressRefreshToggle = false;
175
176 @action
177 String getFreshAddress() {
178 addressRefreshToggle = !addressRefreshToggle;
179 return address;
180 }
181
182 @override
183 String get addressForExchange => getFreshAddress();
184
185 @override
186 @computed
187 String get address {
188 final _ = addressRefreshToggle;
189 if (addressPageType == SilentPaymentsAddresType.p2sp) {
190 if (activeSilentAddress != null) {
191 return activeSilentAddress!;
192 }
193
194 return silentAddress.toString();
195 }
196
197 if (addressPageType == LightningAddressType.p2l) {
198 return lightningAddress ??
199 "Error: Unable to fetch your Lightning address, please check your network connection.";
200 }
201
202 final typeMatchingAddressesAll =
203 _addresses.where((addr) => !addr.isHidden && _isAddressPageTypeMatch(addr)).toList();
204
205 // Prefer standard derivation addresses for the current/active address,
206 // but keep legacy addresses present in the overall address lists.
207 final typeMatchingAddresses = <BitcoinAddressRecord>[
208 ...typeMatchingAddressesAll.where((a) => !a.isLegacyDerivation),
209 ...typeMatchingAddressesAll.where((a) => a.isLegacyDerivation),
210 ];
211
212 final typeMatchingReceiveAddressesAll = typeMatchingAddressesAll
213 .where((addr) => !addr.isUsed && !hiddenAddresses.contains(addr.address))
214 .toList();
215 final typeMatchingReceiveAddresses = <BitcoinAddressRecord>[
216 ...typeMatchingReceiveAddressesAll.where((a) => !a.isLegacyDerivation),
217 ...typeMatchingReceiveAddressesAll.where((a) => a.isLegacyDerivation),
218 ];
219
220 if (!isEnabledAutoGenerateSubaddress) {
221 if (previousAddressRecord != null && previousAddressRecord!.type == addressPageType) {
222 return previousAddressRecord!.address;
223 }
224
225 if (typeMatchingAddresses.isNotEmpty) {
226 return typeMatchingAddresses.first.address;
227 }
228
229 return generateNewAddress().address;
230 }
231
232 if (typeMatchingAddresses.isEmpty || typeMatchingReceiveAddresses.isEmpty) {
233 return generateNewAddress().address;
234 }
235
236 final locked = lockedReceiveAddressByType[addressPageType];
237 if (locked != null && !hiddenAddresses.contains(locked)) return locked;
238
239 final prev = previousAddressRecord;
240 if (prev != null &&
241 prev.type == addressPageType &&
242 !prev.isUsed &&
243 !prev.isLegacyDerivation &&
244 !hiddenAddresses.contains(prev.address)) {
245 return prev.address;
246 }
247
248 return typeMatchingReceiveAddresses.first.address;
249 }
250
251 @observable
252 bool isEnabledAutoGenerateSubaddress = true;
253
254 @override
255 set address(String addr) {
256 if (addr == "Silent Payments" && SilentPaymentsAddresType.p2sp != addressPageType) {
257 return;
258 }
259 if (addressPageType == SilentPaymentsAddresType.p2sp) {
260 final selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr);
261
262 if (selected.silentPaymentTweak != null && silentAddress != null) {
263 activeSilentAddress =
264 silentAddress!.toLabeledSilentPaymentAddress(selected.index).toString();
265 } else {
266 activeSilentAddress = silentAddress!.toString();
267 }
268 return;
269 }
270 try {
271 final addressRecord = _addresses.firstWhere(
272 (addressRecord) => addressRecord.address == addr && !addressRecord.isLegacyDerivation,
273 orElse: () => _addresses.firstWhere((r) => r.address == addr),
274 );
275
276 lockedReceiveAddressByType.remove(addressPageType);
277
278 previousAddressRecord = addressRecord;
279 receiveAddresses.remove(addressRecord);
280 receiveAddresses.insert(0, addressRecord);
281
282 if (isEnabledAutoGenerateSubaddress &&
283 addressRecord.isUsed &&
284 addressRecord.type == addressPageType) {
285 lockedReceiveAddressByType[addressPageType] = addr;
286 }
287 } catch (e) {
288 printV("ElectrumWalletAddressBase: set address ($addr): $e");
289 }
290 }
291
292 @action
293 void clearLockIfMatches(BitcoinAddressType type, String address) {
294 final locked = lockedReceiveAddressByType[type];
295 if (locked != null && locked == address) {
296 lockedReceiveAddressByType.remove(type);
297 }
298 }
299
300 @override
301 String get primaryAddress {
302 if (addressPageType == SilentPaymentsAddresType.p2sp) {
303 return silentAddress?.toString() ?? '';
304 }
305
306 final mainHd = mainHdByType[addressPageType] ?? mainHdByType.values.first;
307 return getAddress(index: 0, hd: mainHd, addressType: addressPageType);
308 }
309
310 String get payjoinCompatibleAddress {
311 final addrType = (addressPageType == SilentPaymentsAddresType.p2sp ||
312 addressPageType == LightningAddressType.p2l)
313 ? SegwitAddresType.p2wpkh
314 : addressPageType;
315
316 final mainHd = mainHdByType[addrType] ?? mainHdByType.values.first;
317 return getAddress(index: 0, hd: mainHd, addressType: addrType);
318 }
319
320 Map<String, int> currentReceiveAddressIndexByType;
321
322 int get currentReceiveAddressIndex =>
323 currentReceiveAddressIndexByType[_addressPageType.toString()] ?? 0;
324
325 void set currentReceiveAddressIndex(int index) =>
326 currentReceiveAddressIndexByType[_addressPageType.toString()] = index;
327
328 Map<String, int> currentChangeAddressIndexByType;
329
330 int get currentChangeAddressIndex =>
331 currentChangeAddressIndexByType[_addressPageType.toString()] ?? 0;
332
333 void set currentChangeAddressIndex(int index) =>
334 currentChangeAddressIndexByType[_addressPageType.toString()] = index;
335
336 int currentSilentAddressIndex;
337
338 @observable
339 BitcoinAddressRecord? previousAddressRecord;
340
341 @computed
342 int get totalCountOfReceiveAddresses => addressesByReceiveType.fold(0, (acc, addressRecord) {
343 if (!addressRecord.isHidden) {
344 return acc + 1;
345 }
346 return acc;
347 });
348
349 @computed
350 int get totalCountOfChangeAddresses => addressesByReceiveType.fold(0, (acc, addressRecord) {
351 if (addressRecord.isHidden) {
352 return acc + 1;
353 }
354 return acc;
355 });
356
357 @override
358 Future<void> init() async {
359 if (walletInfo.type == WalletType.bitcoinCash) {
360 await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
361 } else if (walletInfo.type == WalletType.litecoin) {
362 await _generateInitialAddresses(type: SegwitAddresType.p2wpkh);
363 if ((Platform.isAndroid || Platform.isIOS) && !isHardwareWallet) {
364 await _generateInitialAddresses(type: SegwitAddresType.mweb);
365 }
366 } else if (walletInfo.type == WalletType.dogecoin) {
367 await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
368 } else if (walletInfo.type == WalletType.bitcoin) {
369 await _generateInitialAddresses(isLegacyDerivation: true);
370 await _generateInitialAddresses();
371 if (!isHardwareWallet) {
372 await _generateInitialAddresses(type: P2pkhAddressType.p2pkh, isLegacyDerivation: true);
373 await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
374
375 await _generateInitialAddresses(
376 type: P2shAddressType.p2wpkhInP2sh, isLegacyDerivation: true);
377 await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh);
378
379 await _generateInitialAddresses(type: SegwitAddresType.p2tr, isLegacyDerivation: true);
380 await _generateInitialAddresses(type: SegwitAddresType.p2tr);
381
382 await _generateInitialAddresses(type: SegwitAddresType.p2wsh, isLegacyDerivation: true);
383 await _generateInitialAddresses(type: SegwitAddresType.p2wsh);
384 }
385 }
386
387 updateAddressesByMatch();
388 updateReceiveAddresses();
389 updateChangeAddresses();
390 _validateAddresses();
391 await updateAddressesInBox();
392
393 if (currentReceiveAddressIndex >= receiveAddresses.length) {
394 currentReceiveAddressIndex = 0;
395 }
396
397 if (currentChangeAddressIndex >= changeAddresses.length) {
398 currentChangeAddressIndex = 0;
399 }
400 }
401
402 @action
403 Future<BitcoinAddressRecord> getChangeAddress(
404 {List<BitcoinUnspent>? inputs,
405 List<BitcoinOutput>? outputs,
406 UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) async {
407 updateChangeAddresses();
408
409 if (changeAddresses.isEmpty) {
410 final newAddresses = await _createNewAddresses(gap,
411 startIndex: totalCountOfChangeAddresses > 0 ? totalCountOfChangeAddresses - 1 : 0,
412 isHidden: true);
413 addAddresses(newAddresses);
414 }
415
416 if (currentChangeAddressIndex >= changeAddresses.length) {
417 currentChangeAddressIndex = 0;
418 }
419
420 updateChangeAddresses();
421 final address = changeAddresses[currentChangeAddressIndex];
422 currentChangeAddressIndex += 1;
423 return address;
424 }
425
426 Map<String, String> get labels {
427 final G = ECPublic.fromBytes(BigintUtils.toBytes(Curves.generatorSecp256k1.x, length: 32));
428 final labels = <String, String>{};
429 for (int i = 0; i < silentAddresses.length; i++) {
430 final silentAddressRecord = silentAddresses[i];
431 final silentPaymentTweak = silentAddressRecord.silentPaymentTweak;
432
433 if (silentPaymentTweak != null &&
434 SilentPaymentAddress.regex.hasMatch(silentAddressRecord.address)) {
435 labels[G
436 .tweakMul(BigintUtils.fromBytes(BytesUtils.fromHexString(silentPaymentTweak)))
437 .toHex()] = silentPaymentTweak;
438 }
439 }
440 return labels;
441 }
442
443 @action
444 BaseBitcoinAddressRecord generateNewAddress({String label = ''}) {
445 if (addressPageType is LightningAddressType) {
446 throw Exception("Lightning addresses cannot be rotated");
447 }
448
449 if (addressPageType == SilentPaymentsAddresType.p2sp && silentAddress != null) {
450 final currentSilentAddressIndex = silentAddresses
451 .where((addressRecord) => addressRecord.type != SegwitAddresType.p2tr)
452 .length -
453 1;
454
455 this.currentSilentAddressIndex = currentSilentAddressIndex;
456
457 final address = BitcoinSilentPaymentAddressRecord(
458 silentAddress!.toLabeledSilentPaymentAddress(currentSilentAddressIndex).toString(),
459 index: currentSilentAddressIndex,
460 isHidden: false,
461 name: label,
462 silentPaymentTweak:
463 BytesUtils.toHexString(silentAddress!.generateLabel(currentSilentAddressIndex)),
464 network: network,
465 type: SilentPaymentsAddresType.p2sp,
466 );
467
468 silentAddresses.add(address);
469 Future.delayed(Duration.zero, () => updateAddressesByMatch());
470
471 return address;
472 }
473
474 final newAddressIndex = addressesByReceiveType.fold(
475 0, (int acc, addressRecord) => addressRecord.isHidden == false ? acc + 1 : acc);
476
477 final hd = _hdFor(isHidden: false, type: addressPageType, isLegacyDerivation: false);
478 final address = BitcoinAddressRecord(
479 getAddress(index: newAddressIndex, hd: hd, addressType: addressPageType),
480 index: newAddressIndex,
481 isHidden: false,
482 isLegacyDerivation: false,
483 name: label,
484 type: addressPageType,
485 network: network,
486 );
487 Future.delayed(Duration.zero, () {
488 _addresses.add(address);
489 updateAddressesByMatch();
490 });
491 return address;
492 }
493
494 String getAddress({
495 required int index,
496 required Bip32Slip10Secp256k1 hd,
497 BitcoinAddressType? addressType,
498 }) =>
499 '';
500
501 Future<String> getAddressAsync({
502 required int index,
503 required Bip32Slip10Secp256k1 hd,
504 BitcoinAddressType? addressType,
505 }) async =>
506 getAddress(index: index, hd: hd, addressType: addressType);
507
508 void addBitcoinAddressTypes() {
509 final lastP2wpkh = _addresses
510 .where((addressRecord) =>
511 _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh))
512 .toList()
513 .last;
514 if (lastP2wpkh.address != address) {
515 addressesMap[lastP2wpkh.address] = 'P2WPKH';
516 } else {
517 addressesMap[address] = 'Active - P2WPKH';
518 }
519
520 final lastP2pkh = _addresses.firstWhere(
521 (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh));
522 if (lastP2pkh.address != address) {
523 addressesMap[lastP2pkh.address] = 'P2PKH';
524 } else {
525 addressesMap[address] = 'Active - P2PKH';
526 }
527
528 final lastP2sh = _addresses.firstWhere((addressRecord) =>
529 _isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh));
530 if (lastP2sh.address != address) {
531 addressesMap[lastP2sh.address] = 'P2SH';
532 } else {
533 addressesMap[address] = 'Active - P2SH';
534 }
535
536 final lastP2tr = _addresses.firstWhere(
537 (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr));
538 if (lastP2tr.address != address) {
539 addressesMap[lastP2tr.address] = 'P2TR';
540 } else {
541 addressesMap[address] = 'Active - P2TR';
542 }
543
544 final lastP2wsh = _addresses.firstWhere(
545 (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh));
546 if (lastP2wsh.address != address) {
547 addressesMap[lastP2wsh.address] = 'P2WSH';
548 } else {
549 addressesMap[address] = 'Active - P2WSH';
550 }
551
552 final firstSilentAddressRecord = silentAddresses.firstOrNull;
553 if (firstSilentAddressRecord != null) {
554 if (firstSilentAddressRecord.address != address) {
555 addressesMap[firstSilentAddressRecord.address] = firstSilentAddressRecord.name.isEmpty
556 ? "Silent Payments"
557 : "Silent Payments - ${firstSilentAddressRecord.name}";
558 } else {
559 addressesMap[address] = 'Active - Silent Payments';
560 }
561 }
562
563 if (lightningAddress != null) {
564 if (lightningAddress != address) {
565 addressesMap[lightningAddress!] = 'LN';
566 } else {
567 addressesMap[address] = 'Active - LN';
568 }
569 }
570 }
571
572 void addLitecoinAddressTypes() {
573 final lastP2wpkh = _addresses
574 .where((addressRecord) =>
575 _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh))
576 .toList()
577 .last;
578 if (lastP2wpkh.address != address) {
579 addressesMap[lastP2wpkh.address] = 'P2WPKH';
580 } else {
581 addressesMap[address] = 'Active - P2WPKH';
582 }
583
584 final lastMweb = _addresses.firstWhere(
585 (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.mweb));
586 if (lastMweb.address != address) {
587 addressesMap[lastMweb.address] = 'MWEB';
588 } else {
589 addressesMap[address] = 'Active - MWEB';
590 }
591 }
592
593 void addP2PKHAddressTypes() {
594 final lastP2pkh = _addresses.firstWhere(
595 (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh));
596 if (lastP2pkh.address != address) {
597 addressesMap[lastP2pkh.address] = 'P2PKH';
598 } else {
599 addressesMap[address] = 'Active - P2PKH';
600 }
601 }
602
603 @override
604 Future<void> updateAddressesInBox() async {
605 try {
606 addressesMap.clear();
607 addressesMap[address] = 'Active';
608
609 allAddressesMap.clear();
610 _addresses.forEach((addressRecord) {
611 allAddressesMap[addressRecord.address] = addressRecord.name;
612 });
613
614 switch (walletInfo.type) {
615 case WalletType.bitcoin:
616 addBitcoinAddressTypes();
617 break;
618 case WalletType.litecoin:
619 addLitecoinAddressTypes();
620 break;
621 case WalletType.bitcoinCash:
622 addP2PKHAddressTypes();
623 break;
624 case WalletType.dogecoin:
625 addP2PKHAddressTypes();
626 break;
627 default:
628 break;
629 }
630
631 await saveAddressesInBox();
632 } catch (e) {
633 printV("updateAddresses $e");
634 }
635 }
636
637 @action
638 void updateAddress(String address, String label) {
639 BaseBitcoinAddressRecord? foundAddress;
640 _addresses.forEach((addressRecord) {
641 if (addressRecord.address == address) {
642 foundAddress = addressRecord;
643 }
644 });
645 silentAddresses.forEach((addressRecord) {
646 if (addressRecord.address == address) {
647 foundAddress = addressRecord;
648 }
649 });
650 mwebAddresses.forEach((addressRecord) {
651 if (addressRecord.address == address) {
652 foundAddress = addressRecord;
653 }
654 });
655
656 if (foundAddress != null) {
657 foundAddress!.setNewName(label);
658
659 if (foundAddress is BitcoinAddressRecord) {
660 final index = _addresses.indexOf(foundAddress);
661 _addresses.remove(foundAddress);
662 _addresses.insert(index, foundAddress as BitcoinAddressRecord);
663 } else {
664 final index = silentAddresses.indexOf(foundAddress as BitcoinSilentPaymentAddressRecord);
665 silentAddresses.remove(foundAddress);
666 silentAddresses.insert(index, foundAddress as BitcoinSilentPaymentAddressRecord);
667 }
668 }
669 }
670
671 @action
672 void updateAddressesByMatch() {
673 if (addressPageType == SilentPaymentsAddresType.p2sp) {
674 addressesByReceiveType.clear();
675 addressesByReceiveType.addAll(silentAddresses);
676 return;
677 }
678
679 addressesByReceiveType.clear();
680 addressesByReceiveType.addAll(
681 _addresses.where(_isAddressPageTypeMatch).toList(),
682 );
683 }
684
685 @action
686 void updateReceiveAddresses() {
687 receiveAddresses.removeRange(0, receiveAddresses.length);
688 final newAddresses =
689 _addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed);
690 receiveAddresses.addAll(newAddresses);
691 }
692
693 @action
694 void updateChangeAddresses() {
695 changeAddresses.removeRange(0, changeAddresses.length);
696 final newAddresses = _addresses.where((addressRecord) =>
697 addressRecord.isHidden &&
698 !addressRecord.isUsed &&
699 // TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type
700 (walletInfo.type != WalletType.bitcoin || addressRecord.type == SegwitAddresType.p2wpkh));
701 changeAddresses.addAll(newAddresses);
702 }
703
704 @action
705 Future<void> discoverAddresses(
706 List<BitcoinAddressRecord> addressList,
707 bool isHidden,
708 Future<String?> Function(BitcoinAddressRecord) getAddressHistory, {
709 BitcoinAddressType type = SegwitAddresType.p2wpkh,
710 required bool isLegacyDerivation,
711 }) async {
712 final newAddresses = await _createNewAddresses(
713 gap,
714 startIndex: addressList.length,
715 isHidden: isHidden,
716 isLegacyDerivation: isLegacyDerivation,
717 type: type,
718 );
719
720 addAddresses(newAddresses);
721 addressList.addAll(newAddresses);
722
723 final addressesWithHistory = await Future.wait(newAddresses.map(getAddressHistory));
724 final isLastAddressUsed = addressesWithHistory.last != null;
725
726 if (isLastAddressUsed) {
727 await discoverAddresses(
728 addressList,
729 isHidden,
730 getAddressHistory,
731 type: type,
732 isLegacyDerivation: isLegacyDerivation,
733 );
734 }
735 }
736
737 @action
738 Future<List<BitcoinAddressRecord>> discoverAddressesBatch(
739 List<BitcoinAddressRecord> addressList,
740 bool isHidden,
741 Future<Set<String>> Function(List<BitcoinAddressRecord>) getUsedAddresses, {
742 BitcoinAddressType type = SegwitAddresType.p2wpkh,
743 required bool isLegacyDerivation,
744 }) async {
745 final newAddresses = await _createNewAddresses(
746 gap,
747 startIndex: addressList.length,
748 isHidden: isHidden,
749 type: type,
750 isLegacyDerivation: isLegacyDerivation,
751 );
752 addAddresses(newAddresses);
753
754 final usedAddresses = await getUsedAddresses(newAddresses);
755
756 final hasUsedAddressInGap =
757 newAddresses.any((addressRecord) => usedAddresses.contains(addressRecord.address));
758
759 if (!hasUsedAddressInGap) {
760 return newAddresses;
761 }
762
763 final updatedAddressList = [...addressList, ...newAddresses];
764
765 final moreNewAddresses = await discoverAddressesBatch(
766 updatedAddressList,
767 isHidden,
768 getUsedAddresses,
769 type: type,
770 isLegacyDerivation: isLegacyDerivation,
771 );
772
773 return [...newAddresses, ...moreNewAddresses];
774 }
775
776 Future<void> _generateInitialAddresses(
777 {BitcoinAddressType type = SegwitAddresType.p2wpkh, bool isLegacyDerivation = false}) async {
778 // Legacy derivation produces the same addresses as standard for these types.
779 // Don't generate a legacy set to avoid duplicates.
780 if (isLegacyDerivation && (type == SegwitAddresType.p2wpkh || type == SegwitAddresType.p2wsh)) {
781 return;
782 }
783
784 var countOfReceiveAddresses = 0;
785 var countOfHiddenAddresses = 0;
786
787 _addresses.forEach((addr) {
788 if (addr.type == type && addr.isLegacyDerivation == isLegacyDerivation) {
789 if (addr.isHidden) {
790 countOfHiddenAddresses += 1;
791 } else {
792 countOfReceiveAddresses += 1;
793 }
794 }
795 });
796
797 if (countOfReceiveAddresses < defaultReceiveAddressesCount) {
798 final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses;
799 final newAddresses = await _createNewAddresses(addressesCount,
800 startIndex: countOfReceiveAddresses,
801 isHidden: false,
802 type: type,
803 isLegacyDerivation: isLegacyDerivation);
804 addAddresses(newAddresses);
805 }
806
807 if (countOfHiddenAddresses < defaultChangeAddressesCount) {
808 final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses;
809 final newAddresses = await _createNewAddresses(addressesCount,
810 startIndex: countOfHiddenAddresses,
811 isHidden: true,
812 type: type,
813 isLegacyDerivation: isLegacyDerivation);
814 addAddresses(newAddresses);
815 }
816 }
817
818 Future<List<BitcoinAddressRecord>> _createNewAddresses(int count,
819 {int startIndex = 0,
820 bool isHidden = false,
821 BitcoinAddressType? type,
822 bool isLegacyDerivation = false}) async {
823 final list = <BitcoinAddressRecord>[];
824
825 for (var i = startIndex; i < count + startIndex; i++) {
826 final addrType = type ?? addressPageType;
827 final hd = _hdFor(isHidden: isHidden, type: addrType, isLegacyDerivation: isLegacyDerivation);
828
829 final address = BitcoinAddressRecord(
830 await getAddressAsync(index: i, hd: hd, addressType: addrType),
831 index: i,
832 isHidden: isHidden,
833 isLegacyDerivation: isLegacyDerivation,
834 type: addrType,
835 network: network,
836 );
837 list.add(address);
838 }
839
840 return list;
841 }
842
843 @action
844 void addAddresses(Iterable<BitcoinAddressRecord> addresses) {
845 final addressesSet = this._addresses.toSet();
846 addressesSet.addAll(addresses);
847 this._addresses.clear();
848 this._addresses.addAll(addressesSet);
849 updateAddressesByMatch();
850 updateReceiveAddresses();
851 updateChangeAddresses();
852 }
853
854 @action
855 void addSilentAddresses(Iterable<BitcoinSilentPaymentAddressRecord> addresses) {
856 final addressesSet = this.silentAddresses.toSet();
857 addressesSet.addAll(addresses);
858 this.silentAddresses.clear();
859 this.silentAddresses.addAll(addressesSet);
860 updateAddressesByMatch();
861 }
862
863 @action
864 void addMwebAddresses(Iterable<BitcoinAddressRecord> addresses) {
865 final addressesSet = this.mwebAddresses.toSet();
866 addressesSet.addAll(addresses);
867 this.mwebAddresses.clear();
868 this.mwebAddresses.addAll(addressesSet);
869 updateAddressesByMatch();
870 }
871
872 void _validateAddresses() {
873 _addresses.forEach((element) async {
874 if (element.type == SegwitAddresType.mweb) {
875 // this would add a ton of startup lag for mweb addresses since we have 1000 of them
876 return;
877 }
878
879 final mainHd = _hdFor(
880 isHidden: false, type: element.type, isLegacyDerivation: element.isLegacyDerivation);
881 final sideHd = _hdFor(
882 isHidden: true, type: element.type, isLegacyDerivation: element.isLegacyDerivation);
883 if (!element.isHidden &&
884 element.address !=
885 await getAddressAsync(index: element.index, hd: mainHd, addressType: element.type)) {
886 element.isHidden = true;
887 } else if (element.isHidden &&
888 element.address !=
889 await getAddressAsync(index: element.index, hd: sideHd, addressType: element.type)) {
890 element.isHidden = false;
891 }
892 });
893 }
894
895 @override
896 String get addressForBuy => super.addressForBuy;
897
898 @action
899 Future<void> setAddressType(BitcoinAddressType type) async {
900 _addressPageType = type;
901 updateAddressesByMatch();
902 walletInfo.addressPageType = addressPageType.toString();
903 await walletInfo.save();
904 }
905
906 bool _isAddressPageTypeMatch(BitcoinAddressRecord addressRecord) {
907 return _isAddressByType(addressRecord, addressPageType);
908 }
909
910 bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type;
911
912 bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) =>
913 !addr.isHidden && !addr.isUsed && addr.type == type;
914
915 @action
916 void deleteSilentPaymentAddress(String address) {
917 final addressRecord = silentAddresses.firstWhere((addressRecord) =>
918 addressRecord.type == SilentPaymentsAddresType.p2sp && addressRecord.address == address);
919
920 silentAddresses.remove(addressRecord);
921 updateAddressesByMatch();
922 }
923
924 Bip32Slip10Secp256k1 _hdFor({
925 required bool isHidden,
926 required BitcoinAddressType type,
927 required bool isLegacyDerivation,
928 }) {
929 if (isLegacyDerivation) return isHidden ? legacySideHd : legacyMainHd;
930
931 final map = isHidden ? sideHdByType : mainHdByType;
932 final hd = map[type];
933 if (hd == null) throw Exception("HD not found for type $type");
934 return hd;
935 }
936
937 @action
938 Future<void> setLightningAddress(String walletName, {String newAddress = ""}) async {
939 if (lightningWallet == null) return;
940
941 try {
942 final path = await pathForWalletDir(name: walletName, type: WalletType.bitcoin);
943 final initialized = await lightningWallet!.init(path);
944
945 if (!initialized) {
946 printV("Failed to initialize the lightning wallet");
947 return;
948 }
949
950 lightningAddress = await lightningWallet!.getAddress();
951
952 late final String username;
953
954 if (newAddress.isEmpty) {
955 if (lightningAddress != null) return;
956
957 final randomNumber = Random.secure().nextInt(9999);
958 final randomName = await generateName();
959 username = "${randomName.replaceAll(" ", "")}$randomNumber".toLowerCase();
960 } else {
961 username = newAddress;
962 }
963
964 try {
965 printV(username);
966 lightningAddress = await lightningWallet!.registerAddress(username);
967 } catch (e) {
968 printV(e);
969 printV(username);
970 rethrow;
971 }
972 } on SdkError_NetworkError catch (_) {
973 } on SdkError_SparkError catch (e) {
974 if (!e.field0.contains("dns") && !e.field0.contains("TimedOut")) rethrow;
975 } finally {
976 lightningAddress ??= lightningWallet!.cachedAddress;
977 }
978 }
979 }