dev
dart 271 lines 8.53 KB
Raw
1 import 'dart:async';
2 import 'dart:io' show Platform;
3 import 'dart:typed_data';
4
5 import 'package:bitcoin_base/bitcoin_base.dart';
6 import 'package:blockchain_utils/blockchain_utils.dart';
7 import 'package:cw_bitcoin/bitcoin_address_record.dart';
8 import 'package:cw_bitcoin/bitcoin_receive_page_option.dart';
9 import 'package:cw_bitcoin/bitcoin_unspent.dart';
10 import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
11 import 'package:cw_bitcoin/utils.dart';
12 import 'package:cw_core/payment_uris.dart';
13 import 'package:cw_core/receive_page_option.dart';
14 import 'package:cw_core/unspent_coin_type.dart';
15 import 'package:cw_core/utils/print_verbose.dart';
16 import 'package:cw_core/wallet_info.dart';
17 import 'package:cw_mweb/cw_mweb.dart';
18 import 'package:flutter/foundation.dart';
19 import 'package:mobx/mobx.dart';
20
21 part 'litecoin_wallet_addresses.g.dart';
22
23 class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalletAddresses;
24
25 abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
26 LitecoinWalletAddressesBase(
27 WalletInfo walletInfo, {
28 required super.mainHdByType,
29 required super.sideHdByType,
30 required super.legacyMainHd,
31 required super.legacySideHd,
32 required super.network,
33 required super.isHardwareWallet,
34 required this.mwebHd,
35 required this.mwebEnabled,
36 required this.scanSecretOverride,
37 required this.spendPubkeyOverride,
38 super.initialAddresses,
39 super.initialMwebAddresses,
40 super.initialRegularAddressIndex,
41 super.initialChangeAddressIndex,
42 }) : super(walletInfo) {
43 for (int i = 0; i < mwebAddresses.length; i++) {
44 mwebAddrs.add(mwebAddresses[i].address);
45 }
46 printV("initialized with ${mwebAddrs.length} mweb addresses");
47 }
48
49 final Bip32Slip10Secp256k1? mwebHd;
50 bool mwebEnabled;
51 int mwebTopUpIndex = 1000;
52 List<String> mwebAddrs = [];
53 bool generating = false;
54
55 final String? scanSecretOverride;
56 final String? spendPubkeyOverride;
57
58 List<int> get scanSecret => (scanSecretOverride != null && scanSecretOverride?.isNotEmpty == true)
59 ? hex.decode(scanSecretOverride!)
60 : mwebHd?.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw ?? List.filled(32, 0);
61
62 List<int> get spendPubkey =>
63 (spendPubkeyOverride != null && spendPubkeyOverride?.isNotEmpty == true)
64 ? hex.decode(spendPubkeyOverride!)
65 : mwebHd?.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed ??
66 List.filled(32, 0);
67
68 @override
69 Future<void> init() async {
70 if (!isHardwareWallet) await initMwebAddresses();
71 await super.init();
72 }
73
74 @computed
75 @override
76 List<BitcoinAddressRecord> get allAddresses {
77 return List.from(super.allAddresses)..addAll(mwebAddresses);
78 }
79
80 Future<void> ensureMwebAddressUpToIndexExists(int _index) async {
81 final index = _index + 1;
82 if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) {
83 return null;
84 }
85 if ((scanSecret.length < 1 || scanSecret.reduce((a, b) => a + b) == 0) &&
86 (spendPubkey.length < 1 || spendPubkey.reduce((a, b) => a + b) == 0)) {
87 return null;
88 }
89
90 Uint8List scan = Uint8List.fromList(scanSecret);
91 Uint8List spend = Uint8List.fromList(spendPubkey);
92
93 if (index < mwebAddresses.length && index < mwebAddrs.length) {
94 return;
95 }
96
97 while (generating) {
98 printV("generating.....");
99 // this function was called multiple times in multiple places:
100 await Future.delayed(const Duration(milliseconds: 100));
101 }
102
103 printV("Generating MWEB addresses up to index $index");
104 generating = true;
105 try {
106 while (mwebAddrs.length <= (index + 1)) {
107 final addresses = CwMweb.addresses(scan, spend, mwebAddrs.length, mwebAddrs.length + 50);
108 printV("generated up to index ${mwebAddrs.length}");
109 // sleep for a bit to avoid making the main thread unresponsive:
110 await Future.delayed(Duration(milliseconds: 200));
111 mwebAddrs.addAll(addresses!);
112 }
113 } catch (_) {}
114 generating = false;
115 printV("Done generating MWEB addresses len: ${mwebAddrs.length}");
116
117 // ensure mweb addresses are up to date:
118 // This is the Case if the Litecoin Wallet is a hardware Wallet
119 if (mwebHd == null && scanSecretOverride == null) return;
120
121 if (mwebAddresses.length < mwebAddrs.length) {
122 List<BitcoinAddressRecord> addressRecords = mwebAddrs
123 .asMap()
124 .entries
125 .map((e) => BitcoinAddressRecord(
126 e.value,
127 index: e.key,
128 type: SegwitAddresType.mweb,
129 network: network,
130 ))
131 .toList();
132 addMwebAddresses(addressRecords);
133 printV("set ${addressRecords.length} mweb addresses");
134 }
135 }
136
137 Future<void> initMwebAddresses() async {
138 if (mwebAddrs.length < 1000) {
139 await ensureMwebAddressUpToIndexExists(20);
140 return;
141 }
142 }
143
144 @override
145 String getAddress({
146 required int index,
147 required Bip32Slip10Secp256k1 hd,
148 BitcoinAddressType? addressType,
149 }) {
150 if (addressType == SegwitAddresType.mweb) {
151 if (mwebAddrs.length == 0) {
152 return "";
153 }
154 return hd == legacySideHd ? mwebAddrs[0] : mwebAddrs[index + 1];
155 }
156 return generateP2WPKHAddress(hd: hd, index: index, network: network);
157 }
158
159 @override
160 Future<String> getAddressAsync({
161 required int index,
162 required Bip32Slip10Secp256k1 hd,
163 BitcoinAddressType? addressType,
164 }) async {
165 if (addressType == SegwitAddresType.mweb) {
166 await ensureMwebAddressUpToIndexExists(index);
167 }
168 return getAddress(index: index, hd: hd, addressType: addressType);
169 }
170
171 @action
172 @override
173 Future<BitcoinAddressRecord> getChangeAddress(
174 {List<BitcoinUnspent>? inputs,
175 List<BitcoinOutput>? outputs,
176 UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}) async {
177 // use regular change address on peg in, otherwise use mweb for change address:
178
179 if (!mwebEnabled || coinTypeToSpendFrom == UnspentCoinType.nonMweb) {
180 return super.getChangeAddress();
181 }
182
183 if (inputs != null && outputs != null) {
184 // check if this is a PEGIN:
185 bool outputsToMweb = false;
186 bool comesFromMweb = false;
187
188 for (var i = 0; i < outputs.length; i++) {
189 // TODO: probably not the best way to tell if this is an mweb address
190 // (but it doesn't contain the "mweb" text at this stage)
191 if (outputs[i].address.toAddress(network).length > 110) {
192 outputsToMweb = true;
193 }
194 }
195
196 inputs.forEach((element) {
197 if (!element.isSending || element.isFrozen) {
198 return;
199 }
200 if (element.address.startsWith("ltcmweb")) {
201 comesFromMweb = true;
202 }
203 });
204
205 bool isPegIn = !comesFromMweb && outputsToMweb;
206 bool isNonMweb = !comesFromMweb && !outputsToMweb;
207
208 // use regular change address if it's not an mweb tx or if it's a peg in:
209 if (isPegIn || isNonMweb) {
210 return super.getChangeAddress();
211 }
212 }
213
214 if (mwebEnabled) {
215 await ensureMwebAddressUpToIndexExists(1);
216 if (mwebAddrs.isNotEmpty) {
217 updateChangeAddresses();
218 return BitcoinAddressRecord(
219 mwebAddrs[0],
220 index: 0,
221 type: SegwitAddresType.mweb,
222 network: network,
223 );
224 }
225 }
226
227 return super.getChangeAddress();
228 }
229
230 @override
231 String get addressForExchange {
232 // don't use mweb addresses for exchange refund address:
233
234 final current = getFreshAddress();
235 final bool isMweb =
236 receiveAddresses.any((e) => e.address == current && e.type == SegwitAddresType.mweb);
237
238 if (isMweb) {
239 final segwit = receiveAddresses
240 .where((e) =>
241 e.type == SegwitAddresType.p2wpkh &&
242 !e.isUsed &&
243 !e.isHidden &&
244 !hiddenAddresses.contains(e.address))
245 .map((e) => e.address)
246 .toList();
247
248 return segwit.isNotEmpty ? segwit.first : current;
249 }
250
251 return current;
252 }
253
254 @override
255 List<ReceivePageOption> get receivePageOptions {
256 if (Platform.isLinux || Platform.isMacOS || Platform.isWindows || isHardwareWallet) {
257 return [
258 ...BitcoinReceivePageOption.allLitecoin
259 .where((element) => element != BitcoinReceivePageOption.mweb),
260 ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet)
261 ];
262 }
263 return [
264 ...BitcoinReceivePageOption.allLitecoin,
265 ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet)
266 ];
267 }
268
269 @override
270 PaymentURI getPaymentUri(String amount) => LitecoinURI(amount: amount, address: address);
271 }