dev
dart 445 lines 13.1 KB
Raw
1 import 'dart:async';
2 import 'dart:convert';
3 import 'dart:ffi';
4 import 'dart:isolate';
5 import 'dart:math';
6
7 import 'package:cw_core/amount/money.dart';
8 import 'package:cw_core/crypto_currency.dart';
9 import 'package:cw_core/utils/print_verbose.dart';
10 import 'package:cw_monero/api/account_list.dart';
11 import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart';
12 import 'package:cw_monero/api/get_all_unspent.dart';
13 import 'package:monero/monero.dart' as monero;
14 import 'package:mutex/mutex.dart';
15 import 'package:polyseed/polyseed.dart';
16
17 bool debugMonero = false;
18
19 int getSyncingHeight() {
20 // final height = monero.MONERO_cw_WalletListener_height(getWlptr());
21 if (currentWallet == null) return 0;
22 final h2 = currentWallet!.blockChainHeight();
23 // printV("height: $height / $h2");
24 return h2;
25 }
26
27 bool isNeededToRefresh() {
28 final wl = getWlptr();
29 if (wl == null) return false;
30 final ret = wl.isNeedToRefresh();
31 wl.resetNeedToRefresh();
32 return ret;
33 }
34
35 bool isNewTransactionExist() {
36 final wlptr = getWlptr();
37 if (wlptr == null) return false;
38 final ret = wlptr.isNewTransactionExist();
39 wlptr.resetIsNewTransactionExist();
40 return ret;
41 }
42
43 String getFilename() => currentWallet!.filename();
44
45 String getSeed() {
46 // monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed);
47 final cakepolyseed = currentWallet!.getCacheAttribute(key: "cakewallet.seed");
48 final cakepassphrase = getPassphrase();
49
50 final weirdPolyseed = currentWallet!.getPolyseed(passphrase: cakepassphrase);
51 if (weirdPolyseed != "") return weirdPolyseed;
52
53 if (cakepolyseed != "") {
54 if (cakepassphrase != "") {
55 try {
56 final lang = PolyseedLang.getByPhrase(cakepolyseed);
57 final coin = PolyseedCoin.POLYSEED_MONERO;
58 final ps = Polyseed.decode(cakepolyseed, lang, coin);
59 if (ps.isEncrypted || cakepassphrase == "") return ps.encode(lang, coin);
60 ps.crypt(cakepassphrase);
61 return ps.encode(lang, coin);
62 } catch (e) {
63 printV(e);
64 }
65 }
66 return cakepolyseed;
67 }
68
69 final bip39 = currentWallet!.getCacheAttribute(key: "cakewallet.seed.bip39");
70
71 if (bip39.isNotEmpty) return bip39;
72
73 final legacy = getSeedLegacy(null);
74 return legacy;
75 }
76
77 String? getSeedLanguage(String? language) {
78 switch (language) {
79 case "Chinese (Traditional)":
80 language = "Chinese (simplified)";
81 break;
82 case "Chinese (Simplified)":
83 language = "Chinese (simplified)";
84 break;
85 case "Korean":
86 language = "English";
87 break;
88 case "Czech":
89 language = "English";
90 break;
91 case "Japanese":
92 language = "English";
93 break;
94 }
95 return language;
96 }
97
98 String getSeedLegacy(String? language) {
99 final cakepassphrase = getPassphrase();
100 language = getSeedLanguage(language);
101 var legacy = currentWallet!.seed(seedOffset: cakepassphrase);
102 if (currentWallet!.status() != 0) {
103 if (currentWallet!.errorString().contains("seed_language")) {
104 currentWallet!.setSeedLanguage(language: "English");
105 legacy = currentWallet!.seed(seedOffset: cakepassphrase);
106 }
107 }
108
109 if (language != null) {
110 currentWallet!.setSeedLanguage(language: language);
111 final status = currentWallet!.status();
112 if (status != 0) {
113 final err = currentWallet!.errorString();
114 if (legacy.isNotEmpty) {
115 return "$err\n\n$legacy";
116 }
117 return err;
118 }
119 legacy = currentWallet!.seed(seedOffset: cakepassphrase);
120 }
121
122 if (currentWallet!.status() != 0) {
123 final err = currentWallet!.errorString();
124 if (legacy.isNotEmpty) {
125 return "$err\n\n$legacy";
126 }
127 return err;
128 }
129 return legacy;
130 }
131
132 String getPassphrase() => currentWallet?.getCacheAttribute(key: "cakewallet.passphrase") ?? "";
133
134 Map<int, Map<int, Map<int, String>>> addressCache = {};
135
136 String getAddress({int accountIndex = 0, int addressIndex = 0}) {
137 // this is a workaround for when we switch the wallet pointer,
138 // it should never reach UI but should be good enough to prevent gray screen
139 // or other errors because of forced null check.
140 if (currentWallet == null) return "<wallet not ready ($accountIndex:$addressIndex)>";
141
142 // printV("getaddress: ${accountIndex}/${addressIndex}: ${monero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)}: ${monero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex)}");
143 // this could be a while loop, but I'm in favor of making it if to not cause freezes
144 if (currentWallet!.numSubaddresses(accountIndex: accountIndex) - 1 < addressIndex) {
145 if (currentWallet!.numSubaddressAccounts() < accountIndex) {
146 currentWallet!.addSubaddressAccount();
147 } else {
148 currentWallet!.addSubaddress(accountIndex: accountIndex);
149 }
150 }
151 addressCache[currentWallet!.ffiAddress()] ??= {};
152 addressCache[currentWallet!.ffiAddress()]![accountIndex] ??= {};
153 addressCache[currentWallet!.ffiAddress()]![accountIndex]![addressIndex] ??=
154 currentWallet!.address(accountIndex: accountIndex, addressIndex: addressIndex);
155 return addressCache[currentWallet!.ffiAddress()]![accountIndex]![addressIndex]!;
156 }
157
158 Money getFullBalance({int accountIndex = 0}) =>
159 Money.fromInt(currentWallet?.balance(accountIndex: accountIndex) ?? 0, CryptoCurrency.xmr);
160
161 Money getUnlockedBalance({int accountIndex = 0}) => Money.fromInt(
162 currentWallet?.unlockedBalance(accountIndex: accountIndex) ?? 0, CryptoCurrency.xmr);
163
164 int getCurrentHeight() => currentWallet?.blockChainHeight() ?? 0;
165
166 int cachedNodeHeight = 0;
167 bool isHeightRefreshing = false;
168 int getNodeHeightSync() {
169 if (isHeightRefreshing == false) {
170 if (cachedNodeHeight != 0 && getWlptr()?.height() == 1) {
171 return cachedNodeHeight;
172 }
173 (() async {
174 try {
175 isHeightRefreshing = true;
176 final wptrAddress = currentWallet!.ffiAddress();
177 cachedNodeHeight = await Isolate.run(() async {
178 return monero.Wallet_daemonBlockChainHeight(Pointer.fromAddress(wptrAddress));
179 });
180 } catch (_) {
181 } finally {
182 isHeightRefreshing = false;
183 }
184 })();
185 }
186 return cachedNodeHeight;
187 }
188
189 Future<bool> isConnected() async {
190 final wptrAddress = currentWallet!.ffiAddress();
191 return await Isolate.run(() => monero.Wallet_connected(Pointer.fromAddress(wptrAddress))) == 1;
192 }
193
194 Future<bool> setupNodeSync(
195 {required String address,
196 String? login,
197 String? password,
198 bool useSSL = false,
199 bool isLightWallet = false,
200 String? socksProxyAddress}) async {
201 printV('''
202 {
203 wptr!,
204 daemonAddress: $address,
205 useSsl: $useSSL,
206 proxyAddress: $socksProxyAddress ?? '',
207 daemonUsername: $login ?? '',
208 daemonPassword: $password ?? ''
209 }
210 ''');
211 final addr = currentWallet!.ffiAddress();
212 printV("init: start");
213 await Isolate.run(() {
214 monero.Wallet_init(Pointer.fromAddress(addr),
215 daemonAddress: address,
216 useSsl: useSSL,
217 proxyAddress: socksProxyAddress ?? '',
218 daemonUsername: login ?? '',
219 daemonPassword: password ?? '');
220 });
221 printV("init: end");
222
223 final status = currentWallet!.status();
224
225 if (status != 0) {
226 final error = currentWallet!.errorString();
227 if (error != "no tx keys found for this txid") {
228 printV("error: $error");
229 throw SetupWalletException(message: error);
230 }
231 }
232
233 if (true) {
234 currentWallet!.init3(
235 argv0: '',
236 defaultLogBaseName: 'moneroc',
237 console: true,
238 logPath: '',
239 );
240 }
241
242 return status == 0;
243 }
244
245 void startRefreshSync() {
246 currentWallet!.startRefresh();
247 }
248
249 void setRefreshFromBlockHeight({required int height}) {
250 currentWallet!.setRefreshFromBlockHeight(refresh_from_block_height: height);
251 }
252
253 void setRecoveringFromSeed({required bool isRecovery}) {
254 currentWallet!.setRecoveringFromSeed(recoveringFromSeed: isRecovery);
255 currentWallet!.store();
256 }
257
258 final storeMutex = Mutex();
259
260 void refresh() {
261 currentWallet!.refresh();
262 }
263
264 int lastStorePointer = 0;
265 int lastStoreHeight = 0;
266 void storeSync({bool force = false}) async {
267 final addr = currentWallet!.ffiAddress();
268 final synchronized = await Isolate.run(() {
269 return monero.Wallet_synchronized(Pointer.fromAddress(addr));
270 });
271 if (lastStorePointer == addr &&
272 lastStoreHeight + 75000 > currentWallet!.blockChainHeight() &&
273 !synchronized &&
274 !force) {
275 return;
276 }
277 lastStorePointer = currentWallet!.ffiAddress();
278 lastStoreHeight = currentWallet!.blockChainHeight();
279 await storeMutex.acquire();
280 await Isolate.run(() {
281 monero.Wallet_store(Pointer.fromAddress(addr));
282 });
283 storeMutex.release();
284 }
285
286 void setPasswordSync(String password) {
287 currentWallet!.setPassword(password: password);
288
289 final status = currentWallet!.status();
290 if (status != 0) {
291 throw Exception(currentWallet!.errorString());
292 }
293 }
294
295 void closeCurrentWallet() {
296 currentWallet!.stop();
297 }
298
299 String getSecretViewKey() => currentWallet?.secretViewKey() ?? "";
300
301 String getPublicViewKey() => currentWallet?.publicViewKey() ?? "";
302
303 String getSecretSpendKey() => currentWallet?.secretSpendKey() ?? "";
304
305 String getPublicSpendKey() => currentWallet?.publicSpendKey() ?? "";
306
307 class SyncListener {
308 SyncListener(this.onNewBlock, this.onNewTransaction)
309 : _cachedBlockchainHeight = 0,
310 _lastKnownBlockHeight = 0,
311 _initialSyncHeight = 0 {
312 _start();
313 }
314
315 void Function(int, int, double) onNewBlock;
316 void Function() onNewTransaction;
317
318 Timer? _updateSyncInfoTimer;
319 int _cachedBlockchainHeight;
320 int _lastKnownBlockHeight;
321 int _initialSyncHeight;
322
323 Future<int> getNodeHeightOrUpdate(int baseHeight) async {
324 if (_cachedBlockchainHeight < baseHeight || _cachedBlockchainHeight == 0) {
325 _cachedBlockchainHeight = await getNodeHeight();
326 }
327
328 return _cachedBlockchainHeight;
329 }
330
331 void _start() {
332 _cachedBlockchainHeight = 0;
333 _lastKnownBlockHeight = 0;
334 _initialSyncHeight = 0;
335 _updateSyncInfoTimer ??= Timer.periodic(Duration(milliseconds: 1200), (_) async {
336 if (isNewTransactionExist()) {
337 onNewTransaction();
338 }
339
340 var syncHeight = getSyncingHeight();
341
342 if (syncHeight <= 0) {
343 syncHeight = getCurrentHeight();
344 }
345
346 if (_initialSyncHeight <= 0) {
347 _initialSyncHeight = syncHeight;
348 }
349
350 // in case when node didn't report new height yet
351 // it is a workaround for moving height request to another isolate
352 final nodeHeight = await getNodeHeightOrUpdate(syncHeight);
353 if (nodeHeight == 0) {
354 return;
355 }
356 final bchHeight = max(nodeHeight, syncHeight);
357 // printV("syncHeight: $syncHeight, _lastKnownBlockHeight: $_lastKnownBlockHeight, bchHeight: $bchHeight");
358 if (_lastKnownBlockHeight == syncHeight) {
359 return;
360 }
361
362 _lastKnownBlockHeight = syncHeight;
363 final track = bchHeight - _initialSyncHeight;
364 final diff = track - (bchHeight - syncHeight);
365 final ptc = diff <= 0 ? 0.0 : diff / track;
366 final left = bchHeight - syncHeight;
367
368 if ((syncHeight < 0 || left < 0)) {
369 printV("not calling onNewBlock: syncHeight: $syncHeight, left: $left");
370 return;
371 }
372
373 // 1. Actual new height; 2. Blocks left to finish; 3. Progress in percents;
374 onNewBlock.call(syncHeight, left, ptc);
375 });
376 }
377
378 void stop() => _updateSyncInfoTimer?.cancel();
379 }
380
381 SyncListener setListeners(
382 void Function(int, int, double) onNewBlock, void Function() onNewTransaction) {
383 final listener = SyncListener(onNewBlock, onNewTransaction);
384 // setListenerNative();
385 return listener;
386 }
387
388 void onStartup() {}
389
390 void _storeSync(Object _) => storeSync();
391
392 Future<bool> _setupNodeSync(Map<String, Object?> args) async {
393 final address = args['address'] as String;
394 final login = (args['login'] ?? '') as String;
395 final password = (args['password'] ?? '') as String;
396 final useSSL = args['useSSL'] as bool;
397 final isLightWallet = args['isLightWallet'] as bool;
398 final socksProxyAddress = (args['socksProxyAddress'] ?? '') as String;
399
400 return setupNodeSync(
401 address: address,
402 login: login,
403 password: password,
404 useSSL: useSSL,
405 isLightWallet: isLightWallet,
406 socksProxyAddress: socksProxyAddress);
407 }
408
409 void startRefresh() => startRefreshSync();
410
411 Future<void> store() async => _storeSync(0);
412
413 Future<int> getNodeHeight() async => getNodeHeightSync();
414
415 void rescanBlockchainAsync() => currentWallet!.rescanBlockchainAsync();
416
417 String getSubaddressLabel(int accountIndex, int addressIndex) {
418 return currentWallet!.getSubaddressLabel(accountIndex: accountIndex, addressIndex: addressIndex);
419 }
420
421 Future setTrustedDaemon(bool trusted) async => currentWallet!.setTrustedDaemon(arg: trusted);
422
423 Future<bool> trustedDaemon() async => currentWallet!.trustedDaemon();
424
425 String signMessage(String message, {String address = ""}) =>
426 currentWallet!.signMessage(message: message, address: address);
427
428 bool verifyMessage(String message, String address, String signature) =>
429 currentWallet!.verifySignedMessage(message: message, address: address, signature: signature);
430
431 Map<String, List<int>> debugCallLength() => monero.debugCallLength;
432
433 Map<String, dynamic> getWalletCacheDebug() {
434 try {
435 final jsonString =
436 monero.MONERO_Wallet_serializeCacheToJson(Pointer.fromAddress(currentWallet!.ffiAddress()));
437 final blob = json.decode(jsonString);
438 blob['cake:unspent'] = getAllUnspent();
439 return blob;
440 } catch (e) {
441 return {
442 "error": e.toString(),
443 };
444 }
445 }