dev
dart 557 lines 20.3 KB
Raw
1 import 'dart:convert' as convert;
2 import 'dart:ffi';
3 import 'dart:io';
4 import 'dart:isolate';
5
6 import 'package:cw_core/transaction_priority.dart';
7 import 'package:cw_core/utils/print_verbose.dart';
8 import 'package:cw_core/zano_asset.dart';
9 import 'package:cw_zano/api/consts.dart';
10 import 'package:cw_zano/api/model/asset_id_params.dart';
11 import 'package:cw_zano/api/model/create_wallet_result.dart';
12 import 'package:cw_zano/api/model/destination.dart';
13 import 'package:cw_zano/api/model/get_address_info_result.dart';
14 import 'package:cw_zano/api/model/get_recent_txs_and_info_params.dart';
15 import 'package:cw_zano/api/model/get_recent_txs_and_info_result.dart';
16 import 'package:cw_zano/api/model/get_wallet_info_result.dart';
17 import 'package:cw_zano/api/model/get_wallet_status_result.dart';
18 import 'package:cw_zano/api/model/proxy_to_daemon_params.dart';
19 import 'package:cw_zano/api/model/proxy_to_daemon_result.dart';
20 import 'package:cw_zano/api/model/store_result.dart';
21 import 'package:cw_zano/api/model/transfer.dart';
22 import 'package:cw_zano/api/model/transfer_params.dart';
23 import 'package:cw_zano/api/model/transfer_result.dart';
24 import 'package:cw_zano/zano_wallet_exceptions.dart';
25 import 'package:ffi/ffi.dart';
26 import 'package:json_bigint/json_bigint.dart';
27 import 'package:monero/zano.dart' as zano;
28 import 'package:monero/src/generated_bindings_zano.g.dart' as zanoapi;
29 import 'package:path/path.dart' as p;
30
31 mixin ZanoWalletApi {
32 static const _maxReopenAttempts = 5;
33 static const _logInfo = false;
34 static const int _zanoMixinValue = 10;
35
36 int _hWallet = 0;
37
38 int get hWallet => _hWallet;
39
40 set hWallet(int value) {
41 _hWallet = value;
42 }
43
44 int getCurrentTxFee(TransactionPriority priority) =>
45 zano.PlainWallet_getCurrentTxFee(priority.raw);
46
47 void setPassword(String password) => zano.PlainWallet_resetWalletPassword(hWallet, password);
48
49 void closeWallet(int? walletToClose, {bool force = false}) async {
50 printV('close_wallet ${walletToClose ?? hWallet}: $force');
51 if (Platform.isWindows || force) {
52 final result = await _closeWallet(walletToClose ?? hWallet);
53 printV('close_wallet result $result');
54 openWalletCache.removeWhere((_, cwr) => cwr.walletId == (walletToClose ?? hWallet));
55 }
56 }
57
58 static bool isInit = false;
59
60 Future<bool> initWallet() async {
61 if (isInit) return true;
62 final result = zano.PlainWallet_init("", "", 0);
63 isInit = true;
64 return result == "OK";
65 }
66
67 Future<bool> setupNode(String nodeUrl) async {
68 await _setupNode(hWallet, nodeUrl);
69 return true;
70 }
71
72 Future<Directory> getWalletDir() async {
73 final walletInfoResult = await getWalletInfo();
74 return Directory(p.dirname(walletInfoResult.wi.path));
75 }
76
77 Future<File> _getWalletSecretsFile() async {
78 final dir = await getWalletDir();
79 final file = File(p.join(dir.path, "zano-secrets.json.bin"));
80 return file;
81 }
82
83 Future<Map<String, dynamic>> _getSecrets() async {
84 final file = await _getWalletSecretsFile();
85 if (!file.existsSync()) {
86 return {};
87 }
88 final data = file.readAsBytesSync();
89 final b64 = convert.base64.encode(data);
90 final respStr = await invokeMethod("decrypt_data", {"buff": "$b64"});
91 final resp = convert.json.decode(respStr);
92 final dataBytes = convert.base64.decode(resp["result"]["res_buff"] as String);
93 final dataStr = convert.utf8.decode(dataBytes);
94 final dataObject = convert.json.decode(dataStr);
95 return dataObject as Map<String, dynamic>;
96 }
97
98 Future<void> _setSecrets(Map<String, dynamic> data) async {
99 final dataStr = convert.json.encode(data);
100 final b64 = convert.base64.encode(convert.utf8.encode(dataStr));
101 final respStr = await invokeMethod("encrypt_data", {"buff": "$b64"});
102 final resp = convert.json.decode(respStr);
103 final dataBytes = convert.base64.decode(resp["result"]["res_buff"] as String);
104 final file = await _getWalletSecretsFile();
105 file.writeAsBytesSync(dataBytes);
106 }
107
108 Future<String?> _getWalletSecret(String key) async {
109 final secrets = await _getSecrets();
110 return secrets[key] as String?;
111 }
112
113 Future<void> _setWalletSecret(String key, String value) async {
114 final secrets = await _getSecrets();
115 secrets[key] = value;
116 await _setSecrets(secrets);
117 }
118
119 Future<String?> getPassphrase() async {
120 return await _getWalletSecret("passphrase");
121 }
122
123 Future<void> setPassphrase(String passphrase) {
124 return _setWalletSecret("passphrase", passphrase);
125 }
126
127 Future<String> getSeed() async {
128 final passphrase = await getPassphrase();
129 final respStr = await invokeMethod("get_restore_info", {"seed_password": passphrase ?? ""});
130 final resp = convert.json.decode(respStr);
131 return resp["result"]["seed_phrase"] as String;
132 }
133
134 Future<GetWalletInfoResult> getWalletInfo() async {
135 final json = await _getWalletInfo(hWallet);
136 final result = GetWalletInfoResult.fromJson(jsonDecode(json));
137 printV('get_wallet_info got ${result.wi.balances.length} balances: ${result.wi.balances}');
138 return result;
139 }
140
141 Future<GetWalletStatusResult> getWalletStatus() async {
142 final json = await _getWalletStatus(hWallet);
143 if (json == Consts.errorWalletWrongId) {
144 printV('wrong wallet id');
145 throw ZanoWalletException('Wrong wallet id');
146 }
147 final status = GetWalletStatusResult.fromJson(jsonDecode(json));
148 if (_logInfo)
149 printV(
150 'get_wallet_status connected: ${status.isDaemonConnected} in refresh: ${status.isInLongRefresh} progress: ${status.progress} wallet state: ${status.walletState} sync: ${status.currentWalletHeight}/${status.currentDaemonHeight} ${(status.currentWalletHeight / status.currentDaemonHeight * 100).toStringAsFixed(2)}%');
151 return status;
152 }
153
154 Future<String> invokeMethod(String methodName, Object params) async {
155 final request = jsonEncode({
156 "method": methodName,
157 "params": params,
158 });
159 final invokeResult = await callSyncMethod('invoke', hWallet, request);
160 try {
161 jsonDecode(invokeResult);
162 } catch (e) {
163 if (invokeResult.contains(Consts.errorWalletWrongId))
164 throw ZanoWalletException('Wrong wallet id');
165 printV('exception in parsing json in invokeMethod: $invokeResult');
166 rethrow;
167 }
168 return invokeResult;
169 }
170
171 Future<List<ZanoAsset>> getAssetsWhitelist() async {
172 try {
173 final json = await invokeMethod('assets_whitelist_get', '{}');
174 final map = jsonDecode(json) as Map<String, dynamic>?;
175 _checkForErrors(map);
176 List<ZanoAsset> assets(String type, bool isGlobalWhitelist) =>
177 (map?['result']?[type] as List<dynamic>?)
178 ?.map((e) => ZanoAsset.fromJson(e as Map<String, dynamic>,
179 isInGlobalWhitelist: isGlobalWhitelist))
180 .toList() ??
181 [];
182 final localWhitelist = assets('local_whitelist', false);
183 final globalWhitelist = assets('global_whitelist', true);
184 final ownAssets = assets('own_assets', false);
185 if (_logInfo)
186 printV(
187 'assets_whitelist_get got local whitelist: ${localWhitelist.length} ($localWhitelist); '
188 'global whitelist: ${globalWhitelist.length} ($globalWhitelist); '
189 'own assets: ${ownAssets.length} ($ownAssets)');
190 return [...globalWhitelist, ...localWhitelist, ...ownAssets];
191 } catch (e) {
192 printV('assets_whitelist_get $e');
193 return [];
194 // rethrow;
195 }
196 }
197
198 Future<ZanoAsset?> addAssetsWhitelist(String assetId) async {
199 try {
200 final json = await invokeMethod('assets_whitelist_add', AssetIdParams(assetId: assetId));
201 final map = jsonDecode(json) as Map<String, dynamic>?;
202 _checkForErrors(map);
203 if (map!['result']!['status']! == 'OK') {
204 final assetDescriptor =
205 ZanoAsset.fromJson(map['result']!['asset_descriptor']! as Map<String, dynamic>);
206 printV('assets_whitelist_add added ${assetDescriptor.fullName} ${assetDescriptor.ticker}');
207 return assetDescriptor;
208 } else {
209 printV('assets_whitelist_add status ${map['result']!['status']!}');
210 return null;
211 }
212 } catch (e) {
213 printV('assets_whitelist_add $e');
214 return null;
215 }
216 }
217
218 Future<bool> removeAssetsWhitelist(String assetId) async {
219 try {
220 final json = await invokeMethod('assets_whitelist_remove', AssetIdParams(assetId: assetId));
221 final map = jsonDecode(json) as Map<String, dynamic>?;
222 _checkForErrors(map);
223 printV('assets_whitelist_remove status ${map!['result']!['status']!}');
224 return (map['result']!['status']! == 'OK');
225 } catch (e) {
226 printV('assets_whitelist_remove $e');
227 return false;
228 }
229 }
230
231 Future<ProxyToDaemonResult?> _proxyToDaemon(String uri, String body) async {
232 final json = await invokeMethod('proxy_to_daemon', ProxyToDaemonParams(body: body, uri: uri));
233 final map = jsonDecode(json) as Map<String, dynamic>?;
234 _checkForErrors(map);
235 return ProxyToDaemonResult.fromJson(map!['result'] as Map<String, dynamic>);
236 }
237
238 Future<ZanoAsset?> getAssetInfo(String assetId) async {
239 final methodName = 'get_asset_info';
240 final params = AssetIdParams(assetId: assetId);
241 final result = await _proxyToDaemon(
242 '/json_rpc', '{"method": "$methodName","params": ${jsonEncode(params)}}');
243 if (result == null) {
244 printV('get_asset_info empty result');
245 return null;
246 }
247 try {
248 final map = jsonDecode(result.body) as Map<String, dynamic>?;
249 if (map!['error'] != null) {
250 printV(
251 'get_asset_info $assetId error ${map['error']!['code']} ${map['error']!['message']}');
252 return null;
253 } else if (map['result']!['status']! == 'OK') {
254 final assetDescriptor =
255 ZanoAsset.fromJson(map['result']!['asset_descriptor']! as Map<String, dynamic>);
256 printV('get_asset_info $assetId ${assetDescriptor.fullName} ${assetDescriptor.ticker}');
257 return assetDescriptor;
258 } else {
259 printV('get_asset_info $assetId status ${map['result']!['status']!}');
260 return null;
261 }
262 } catch (_) {
263 return null;
264 }
265 }
266
267 Future<StoreResult?> store() async {
268 try {
269 final json = await invokeMethod('store', {});
270 final map = jsonDecode(json) as Map<String, dynamic>?;
271 _checkForErrors(map);
272 return StoreResult.fromJson(map!['result'] as Map<String, dynamic>);
273 } catch (e) {
274 printV('store $e');
275 return null;
276 }
277 }
278
279 Future<GetRecentTxsAndInfoResult> getRecentTxsAndInfo(
280 {required int offset, required int count}) async {
281 printV('get_recent_txs_and_info $offset $count');
282 try {
283 final json = await invokeMethod(
284 'get_recent_txs_and_info', GetRecentTxsAndInfoParams(offset: offset, count: count));
285 final map = jsonDecode(json) as Map<String, dynamic>?;
286 _checkForErrors(map);
287 final lastItemIndex = map?['result']?['last_item_index'] as int?;
288 final totalTransfers = map?['result']?['total_transfers'] as int?;
289 final transfers = map?['result']?['transfers'] as List<dynamic>?;
290 if (transfers == null || lastItemIndex == null || totalTransfers == null) {
291 printV('get_recent_txs_and_info empty transfers');
292 return GetRecentTxsAndInfoResult.empty();
293 }
294 printV('get_recent_txs_and_info transfers.length: ${transfers.length}');
295 return GetRecentTxsAndInfoResult(
296 transfers: transfers.map((e) => Transfer.fromJson(e as Map<String, dynamic>)).toList(),
297 lastItemIndex: lastItemIndex,
298 totalTransfers: totalTransfers,
299 );
300 } catch (e) {
301 printV('get_recent_txs_and_info $e');
302 return GetRecentTxsAndInfoResult.empty();
303 }
304 }
305
306 GetAddressInfoResult getAddressInfo(String address) => GetAddressInfoResult.fromJson(
307 jsonDecode(zano.PlainWallet_getAddressInfo(address)),
308 );
309
310 String _shorten(String s) =>
311 s.length > 10 ? '${s.substring(0, 4)}...${s.substring(s.length - 4)}' : s;
312
313 Future<CreateWalletResult> createWallet(String path, String password) async {
314 printV('create_wallet path $path password ${_shorten(password)}');
315 final json = zano.PlainWallet_generate(path, password);
316 final map = jsonDecode(json) as Map<String, dynamic>?;
317 if (map?['error'] != null) {
318 final code = map!['error']?['code'] ?? '';
319 final message = map['error']?['message'] ?? '';
320 throw ZanoWalletException('Error creating wallet file, $message ($code)');
321 }
322 if (map?['result'] == null) {
323 throw ZanoWalletException('Error creating wallet file, empty response');
324 }
325 final result = CreateWalletResult.fromJson(map!['result'] as Map<String, dynamic>);
326 openWalletCache[path] = result;
327 printV('create_wallet ${result.name}');
328 return result;
329 }
330
331 Future<CreateWalletResult> restoreWalletFromSeed(
332 String path, String password, String seed, String? passphrase) async {
333 printV('restore_wallet path $path');
334 final json = zano.PlainWallet_restore(seed, path, password, passphrase ?? '');
335 final map = jsonDecode(json) as Map<String, dynamic>?;
336 if (map?['error'] != null) {
337 final code = map!['error']!['code'] ?? '';
338 final message = map['error']!['message'] ?? '';
339 if (code == Consts.errorWrongSeed) {
340 throw RestoreFromSeedsException(
341 'Error restoring wallet\nPlease check the seed words are correct. Additionally, if you created this wallet with a passphrase please add it under the Advanced Settings page.');
342 } else if (code == Consts.errorAlreadyExists) {
343 throw RestoreFromSeedsException('Error restoring wallet, already exists');
344 }
345 throw RestoreFromSeedsException('Error restoring wallet, $message ($code)');
346 }
347 if (map?['result'] == null) {
348 throw RestoreFromSeedsException('Error restoring wallet, empty response');
349 }
350 final result = CreateWalletResult.fromJson(map!['result'] as Map<String, dynamic>);
351 openWalletCache[path] = result;
352 printV('restore_wallet ${result.name} ${result.wi.address}');
353 return result;
354 }
355
356 Future<CreateWalletResult> loadWallet(String path, String password, [int attempt = 0]) async {
357 printV('load_wallet1 path $path');
358 final String json;
359 try {
360 json = zano.PlainWallet_open(path, password);
361 } catch (e) {
362 printV('error in loadingWallet $e');
363 rethrow;
364 }
365
366 final map = jsonDecode(json) as Map<String, dynamic>?;
367 if (map?['error'] != null) {
368 final code = map?['error']!['code'] ?? '';
369 final message = map?['error']!['message'] ?? '';
370 if (code == Consts.errorAlreadyExists && attempt <= _maxReopenAttempts) {
371 // already connected to this wallet. closing and trying to reopen
372 printV('already connected. closing and reopen wallet (attempt $attempt)');
373 closeWallet(attempt, force: true);
374 await Future.delayed(const Duration(milliseconds: 500));
375 return await loadWallet(path, password, attempt + 1);
376 }
377 throw ZanoWalletException('Error loading wallet, $message ($code)');
378 }
379 if (map?['result'] == null) {
380 throw ZanoWalletException('Error loading wallet, empty response');
381 }
382 final result = CreateWalletResult.fromJson(map!['result'] as Map<String, dynamic>);
383 printV('load_wallet3 ${result.name} ${result.wi.address}');
384 openWalletCache[path] = result;
385 return result;
386 }
387
388 static Map<String, CreateWalletResult> openWalletCache = {};
389
390 Future<TransferResult> transfer(
391 List<Destination> destinations, BigInt fee, String comment) async {
392 final params = TransferParams(
393 destinations: destinations,
394 fee: fee,
395 mixin: _zanoMixinValue,
396 paymentId: '',
397 comment: comment,
398 pushPayer: false,
399 hideReceiver: true,
400 );
401 final json = await invokeMethod('transfer', params);
402 final map = jsonDecode(json);
403 final resultMap = map as Map<String, dynamic>?;
404 if (resultMap != null) {
405 final transferResultMap = resultMap['result'] as Map<String, dynamic>?;
406 if (transferResultMap != null) {
407 final transferResult = TransferResult.fromJson(transferResultMap);
408 printV('transfer success hash ${transferResult.txHash}');
409 return transferResult;
410 } else {
411 final errorCode = resultMap['error']?['code'];
412 final code = errorCode is int ? errorCode.toString() : errorCode as String? ?? '';
413 final message = resultMap['error']?['message'] as String? ?? '';
414 printV('transfer error $code $message');
415 throw TransferException('Transfer error, $message ($code)');
416 }
417 }
418 printV('transfer error empty result');
419 throw TransferException('Transfer error, empty result');
420 }
421
422 Future<String> signMessage(String message, {String? address = null}) async {
423 try {
424 final messageBase64 = convert.base64.encode(convert.utf8.encode(message));
425 final response = await invokeMethod('sign_message', {'buff': messageBase64});
426 final responseData = convert.jsonDecode(response) as Map<String, dynamic>;
427
428 if (responseData['error'] != null) {
429 printV('ZANO sign_message error: ${responseData['error']}');
430 throw Exception('Zano sign_message failed: ${responseData['error']}');
431 }
432
433 final result = responseData['result'] as Map<String, dynamic>?;
434 if (result == null) {
435 throw Exception('Invalid response from sign_message');
436 }
437
438 final signature = result['sig'] as String?;
439
440 if (signature == null) {
441 throw Exception('No signature in response');
442 }
443
444 return signature;
445 } catch (e) {
446 printV('ZANO signMessage error: $e');
447 rethrow;
448 }
449 }
450
451 void _checkForErrors(Map<String, dynamic>? map) {
452 if (map == null) {
453 throw ZanoWalletException('Empty response');
454 }
455 final result = map['result'];
456 if (result == null) {
457 throw ZanoWalletException('Empty response');
458 }
459 if (result['error'] != null) {
460 final code = result['error']!['code'] ?? '';
461 final message = result['error']!['message'] ?? '';
462 if (code == -1 && message == Consts.errorBusy) {
463 throw ZanoWalletBusyException();
464 }
465 throw ZanoWalletException('Error, $message ($code)');
466 }
467 }
468 }
469
470 Future<String> callSyncMethod(String methodName, int hWallet, String params) async {
471 final params_ = params.toNativeUtf8().address;
472 final method_name_ = methodName.toNativeUtf8().address;
473 final invokeResult = await Isolate.run(() async {
474 final lib = zanoapi.ZanoC(DynamicLibrary.open(zano.libPath));
475 final txid = lib.ZANO_PlainWallet_syncCall(
476 Pointer.fromAddress(method_name_).cast(), hWallet, Pointer.fromAddress(params_).cast());
477 try {
478 final strPtr = txid.cast<Utf8>();
479 final str = strPtr.toDartString();
480 lib.ZANO_free(strPtr.cast());
481 return str;
482 } catch (e) {
483 return "";
484 }
485 });
486 calloc.free(Pointer.fromAddress(method_name_));
487 calloc.free(Pointer.fromAddress(params_));
488 return invokeResult;
489 }
490
491 Map<String, dynamic> jsonDecode(String json) {
492 try {
493 return decodeJson(json.replaceAll("\\/", "/")) as Map<String, dynamic>;
494 } catch (e) {
495 return convert.jsonDecode(json) as Map<String, dynamic>;
496 }
497 }
498
499 String jsonEncode(Object? object) {
500 return convert.jsonEncode(object);
501 }
502
503 Future<String> _getWalletStatus(int hWallet) async {
504 final jsonPtr = await Isolate.run(() async {
505 final lib = zanoapi.ZanoC(DynamicLibrary.open(zano.libPath));
506 final status = lib.ZANO_PlainWallet_getWalletStatus(
507 hWallet,
508 );
509 return status.address;
510 });
511 String json = "";
512 try {
513 final strPtr = Pointer.fromAddress(jsonPtr).cast<Utf8>();
514 final str = strPtr.toDartString();
515 zano.ZANO_free(strPtr.cast());
516 json = str;
517 } catch (e) {
518 json = "";
519 }
520 return json;
521 }
522
523 Future<String> _getWalletInfo(int hWallet) async {
524 final jsonPtr = await Isolate.run(() async {
525 final lib = zanoapi.ZanoC(DynamicLibrary.open(zano.libPath));
526 final status = lib.ZANO_PlainWallet_getWalletInfo(
527 hWallet,
528 );
529 return status.address;
530 });
531 String json = "";
532 try {
533 final strPtr = Pointer.fromAddress(jsonPtr).cast<Utf8>();
534 final str = strPtr.toDartString();
535 zano.ZANO_free(strPtr.cast());
536 json = str;
537 } catch (e) {
538 json = "";
539 }
540 return json;
541 }
542
543 Future<String> _setupNode(int hWallet, String nodeUrl) async {
544 await callSyncMethod("reset_connection_url", hWallet, nodeUrl);
545 await callSyncMethod("run_wallet", hWallet, "");
546 return "OK";
547 }
548
549 Future<String> _closeWallet(int hWallet) async {
550 final str = await Isolate.run(() async {
551 return zano.PlainWallet_closeWallet(hWallet);
552 });
553 printV("Closing wallet: $str");
554 return str;
555 }
556
557 Map<String, List<int>> debugCallLength() => zano.debugCallLength;