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';
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';
25
-import 'package:flutter/foundation.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;
30
mixin ZanoWalletApi {
31
static const _maxReopenAttempts = 5;
32
static const _logInfo = false;
33
- static const _logError = true;
34
- static const _logJson = false;
33
static const int _zanoMixinValue = 10;
34
35
int _hWallet = 0;
44
45
void setPassword(String password) => zano.PlainWallet_resetWalletPassword(hWallet, password);
46
49
- void closeWallet([int? walletToClose]) async {
50
- info('close_wallet ${walletToClose ?? hWallet}');
51
- final result = await _closeWallet(walletToClose ?? hWallet);
52
- info('close_wallet result $result');
47
+ void closeWallet(int? walletToClose, {bool force = false}) async {
48
+ printV('close_wallet ${walletToClose ?? hWallet}');
49
+ if (Platform.isWindows || force) {
50
+ final result = await _closeWallet(walletToClose ?? hWallet);
51
+ printV('close_wallet result $result');
52
+ openWalletCache.removeWhere((_, cwr) => cwr.walletId == (walletToClose ?? hWallet));
53
+ }
54
}
55
56
+ bool isInit = false;
57
+
58
Future<bool> initWallet() async {
59
// pathForWallet(name: , type: type)
60
+ if (isInit) return true;
61
final result = zano.PlainWallet_init("", "", 0);
58
- printV(result);
62
+ isInit = true;
63
return result == "OK";
64
}
65
71
Future<GetWalletInfoResult> getWalletInfo() async {
72
final json = await _getWalletInfo(hWallet);
73
final result = GetWalletInfoResult.fromJson(jsonDecode(json));
70
- _json('get_wallet_info', json);
71
- info('get_wallet_info got ${result.wi.balances.length} balances: ${result.wi.balances} seed: ${_shorten(result.wiExtended.seed)}');
74
+ printV('get_wallet_info got ${result.wi.balances.length} balances: ${result.wi.balances}');
75
return result;
76
}
77
78
Future<GetWalletStatusResult> getWalletStatus() async {
79
final json = await _getWalletStatus(hWallet);
80
if (json == Consts.errorWalletWrongId) {
78
- error('wrong wallet id');
81
+ printV('wrong wallet id');
82
throw ZanoWalletException('Wrong wallet id');
83
}
84
final status = GetWalletStatusResult.fromJson(jsonDecode(json));
82
- _json('get_wallet_status', json);
85
if (_logInfo)
84
- info(
86
+ printV(
87
'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)}%');
88
return status;
89
}
98
jsonDecode(invokeResult);
99
} catch (e) {
100
if (invokeResult.contains(Consts.errorWalletWrongId)) throw ZanoWalletException('Wrong wallet id');
99
- error('exception in parsing json in invokeMethod: $invokeResult');
101
+ printV('exception in parsing json in invokeMethod: $invokeResult');
102
rethrow;
103
}
104
return invokeResult;
107
Future<List<ZanoAsset>> getAssetsWhitelist() async {
108
try {
109
final json = await invokeMethod('assets_whitelist_get', '{}');
108
- _json('assets_whitelist_get', json);
110
final map = jsonDecode(json) as Map<String, dynamic>?;
111
_checkForErrors(map);
112
List<ZanoAsset> assets(String type, bool isGlobalWhitelist) =>
118
final globalWhitelist = assets('global_whitelist', true);
119
final ownAssets = assets('own_assets', false);
120
if (_logInfo)
120
- info('assets_whitelist_get got local whitelist: ${localWhitelist.length} ($localWhitelist); '
121
+ printV('assets_whitelist_get got local whitelist: ${localWhitelist.length} ($localWhitelist); '
122
'global whitelist: ${globalWhitelist.length} ($globalWhitelist); '
123
'own assets: ${ownAssets.length} ($ownAssets)');
124
return [...globalWhitelist, ...localWhitelist, ...ownAssets];
125
} catch (e) {
125
- error('assets_whitelist_get $e');
126
+ printV('assets_whitelist_get $e');
127
return [];
128
// rethrow;
129
}
132
Future<ZanoAsset?> addAssetsWhitelist(String assetId) async {
133
try {
134
final json = await invokeMethod('assets_whitelist_add', AssetIdParams(assetId: assetId));
134
- _json('assets_whitelist_add $assetId', json);
135
final map = jsonDecode(json) as Map<String, dynamic>?;
136
_checkForErrors(map);
137
if (map!['result']!['status']! == 'OK') {
138
final assetDescriptor = ZanoAsset.fromJson(map['result']!['asset_descriptor']! as Map<String, dynamic>);
139
- info('assets_whitelist_add added ${assetDescriptor.fullName} ${assetDescriptor.ticker}');
139
+ printV('assets_whitelist_add added ${assetDescriptor.fullName} ${assetDescriptor.ticker}');
140
return assetDescriptor;
141
} else {
142
- info('assets_whitelist_add status ${map['result']!['status']!}');
142
+ printV('assets_whitelist_add status ${map['result']!['status']!}');
143
return null;
144
}
145
} catch (e) {
146
- error('assets_whitelist_add $e');
146
+ printV('assets_whitelist_add $e');
147
return null;
148
}
149
}
151
Future<bool> removeAssetsWhitelist(String assetId) async {
152
try {
153
final json = await invokeMethod('assets_whitelist_remove', AssetIdParams(assetId: assetId));
154
- _json('assets_whitelist_remove $assetId', json);
154
final map = jsonDecode(json) as Map<String, dynamic>?;
155
_checkForErrors(map);
157
- info('assets_whitelist_remove status ${map!['result']!['status']!}');
156
+ printV('assets_whitelist_remove status ${map!['result']!['status']!}');
157
return (map['result']!['status']! == 'OK');
158
} catch (e) {
160
- error('assets_whitelist_remove $e');
159
+ printV('assets_whitelist_remove $e');
160
return false;
161
}
162
}
172
final methodName = 'get_asset_info';
173
final params = AssetIdParams(assetId: assetId);
174
final result = await _proxyToDaemon('/json_rpc', '{"method": "$methodName","params": ${jsonEncode(params)}}');
176
- _json('$methodName $assetId', result?.body ?? '');
175
if (result == null) {
178
- error('get_asset_info empty result');
176
+ printV('get_asset_info empty result');
177
return null;
178
}
179
final map = jsonDecode(result.body) as Map<String, dynamic>?;
180
if (map!['error'] != null) {
183
- info('get_asset_info $assetId error ${map['error']!['code']} ${map['error']!['message']}');
181
+ printV('get_asset_info $assetId error ${map['error']!['code']} ${map['error']!['message']}');
182
return null;
183
} else if (map['result']!['status']! == 'OK') {
184
final assetDescriptor = ZanoAsset.fromJson(map['result']!['asset_descriptor']! as Map<String, dynamic>);
187
- info('get_asset_info $assetId ${assetDescriptor.fullName} ${assetDescriptor.ticker}');
185
+ printV('get_asset_info $assetId ${assetDescriptor.fullName} ${assetDescriptor.ticker}');
186
return assetDescriptor;
187
} else {
190
- info('get_asset_info $assetId status ${map['result']!['status']!}');
188
+ printV('get_asset_info $assetId status ${map['result']!['status']!}');
189
return null;
190
}
191
}
197
_checkForErrors(map);
198
return StoreResult.fromJson(map!['result'] as Map<String, dynamic>);
199
} catch (e) {
202
- error('store $e');
200
+ printV('store $e');
201
return null;
202
}
203
}
204
205
Future<GetRecentTxsAndInfoResult> getRecentTxsAndInfo({required int offset, required int count}) async {
208
- info('get_recent_txs_and_info $offset $count');
206
+ printV('get_recent_txs_and_info $offset $count');
207
try {
208
final json = await invokeMethod('get_recent_txs_and_info', GetRecentTxsAndInfoParams(offset: offset, count: count));
211
- _json('get_recent_txs_and_info', json);
209
final map = jsonDecode(json) as Map<String, dynamic>?;
210
_checkForErrors(map);
211
final lastItemIndex = map?['result']?['last_item_index'] as int?;
212
final totalTransfers = map?['result']?['total_transfers'] as int?;
213
final transfers = map?['result']?['transfers'] as List<dynamic>?;
214
if (transfers == null || lastItemIndex == null || totalTransfers == null) {
218
- error('get_recent_txs_and_info empty transfers');
215
+ printV('get_recent_txs_and_info empty transfers');
216
return GetRecentTxsAndInfoResult.empty();
217
}
221
- info('get_recent_txs_and_info transfers.length: ${transfers.length}');
218
+ printV('get_recent_txs_and_info transfers.length: ${transfers.length}');
219
return GetRecentTxsAndInfoResult(
220
transfers: transfers.map((e) => Transfer.fromJson(e as Map<String, dynamic>)).toList(),
221
lastItemIndex: lastItemIndex,
222
totalTransfers: totalTransfers,
223
);
224
} catch (e) {
228
- error('get_recent_txs_and_info $e');
225
+ printV('get_recent_txs_and_info $e');
226
return GetRecentTxsAndInfoResult.empty();
227
}
228
}
234
String _shorten(String s) => s.length > 10 ? '${s.substring(0, 4)}...${s.substring(s.length - 4)}' : s;
235
236
Future<CreateWalletResult> createWallet(String path, String password) async {
240
- info('create_wallet path $path password ${_shorten(password)}');
237
+ printV('create_wallet path $path password ${_shorten(password)}');
238
final json = zano.PlainWallet_generate(path, password);
242
- _json('create_wallet', json);
239
final map = jsonDecode(json) as Map<String, dynamic>?;
240
if (map?['error'] != null) {
241
final code = map!['error']?['code'] ?? '';
246
throw ZanoWalletException('Error creating wallet file, empty response');
247
}
248
final result = CreateWalletResult.fromJson(map!['result'] as Map<String, dynamic>);
253
- info('create_wallet ${result.name} ${result.seed}');
249
+ openWalletCache[path] = result;
250
+ printV('create_wallet ${result.name} ${result.seed}');
251
return result;
252
}
253
254
Future<CreateWalletResult> restoreWalletFromSeed(String path, String password, String seed, String? passphrase) async {
258
- info('restore_wallet path $path password ${_shorten(password)} seed ${_shorten(seed)}');
255
+ printV('restore_wallet path $path password ${_shorten(password)} seed ${_shorten(seed)}');
256
final json = zano.PlainWallet_restore(seed, path, password, passphrase??'');
260
- _json('restore_wallet', json);
257
final map = jsonDecode(json) as Map<String, dynamic>?;
258
if (map?['error'] != null) {
259
final code = map!['error']!['code'] ?? '';
269
throw RestoreFromSeedsException('Error restoring wallet, empty response');
270
}
271
final result = CreateWalletResult.fromJson(map!['result'] as Map<String, dynamic>);
276
- info('restore_wallet ${result.name} ${result.wi.address}');
272
+ openWalletCache[path] = result;
273
+ printV('restore_wallet ${result.name} ${result.wi.address}');
274
return result;
275
}
276
277
Future<CreateWalletResult>loadWallet(String path, String password, [int attempt = 0]) async {
281
- info('load_wallet1 path $path password ${_shorten(password)}');
278
+ printV('load_wallet1 path $path password ${_shorten(password)}');
279
final String json;
280
try {
281
json = zano.PlainWallet_open(path, password);
282
} catch (e) {
286
- error('error in loadingWallet $e');
283
+ printV('error in loadingWallet $e');
284
rethrow;
285
}
289
- info('load_wallet2: $json');
286
+ // printV('load_wallet2: $json');
287
final map = jsonDecode(json) as Map<String, dynamic>?;
288
if (map?['error'] != null) {
289
final code = map?['error']!['code'] ?? '';
290
final message = map?['error']!['message'] ?? '';
291
if (code == Consts.errorAlreadyExists && attempt <= _maxReopenAttempts) {
292
// already connected to this wallet. closing and trying to reopen
296
- info('already connected. closing and reopen wallet (attempt $attempt)');
297
- closeWallet(attempt);
293
+ printV('already connected. closing and reopen wallet (attempt $attempt)');
294
+ closeWallet(attempt, force: true);
295
await Future.delayed(const Duration(milliseconds: 500));
296
return await loadWallet(path, password, attempt + 1);
297
}
301
throw ZanoWalletException('Error loading wallet, empty response');
302
}
303
final result = CreateWalletResult.fromJson(map!['result'] as Map<String, dynamic>);
307
- info('load_wallet3 ${result.name} ${result.wi.address}');
304
+ printV('load_wallet3 ${result.name} ${result.wi.address}');
305
+ openWalletCache[path] = result;
306
return result;
307
}
308
309
+ static Map<String, CreateWalletResult> openWalletCache = {};
310
+
311
Future<TransferResult> transfer(List<Destination> destinations, BigInt fee, String comment) async {
312
final params = TransferParams(
313
destinations: destinations,
319
hideReceiver: true,
320
);
321
final json = await invokeMethod('transfer', params);
322
- _json('transfer', json);
322
final map = jsonDecode(json);
323
final resultMap = map as Map<String, dynamic>?;
324
if (resultMap != null) {
325
final transferResultMap = resultMap['result'] as Map<String, dynamic>?;
326
if (transferResultMap != null) {
327
final transferResult = TransferResult.fromJson(transferResultMap);
329
- info('transfer success hash ${transferResult.txHash}');
328
+ printV('transfer success hash ${transferResult.txHash}');
329
return transferResult;
330
} else {
331
final errorCode = resultMap['error']?['code'];
332
final code = errorCode is int ? errorCode.toString() : errorCode as String? ?? '';
333
final message = resultMap['error']?['message'] as String? ?? '';
335
- error('transfer error $code $message');
334
+ printV('transfer error $code $message');
335
throw TransferException('Transfer error, $message ($code)');
336
}
337
}
339
- error('transfer error empty result');
338
+ printV('transfer error empty result');
339
throw TransferException('Transfer error, empty result');
340
}
341
357
}
358
}
359
361
- /*Future<void> _writeLog(String method, String logMessage) async {
362
- final dir = await getDownloadsDirectory();
363
- final logFile = File('${dir!.path}/$method.txt');
364
- final date = DateTime.now();
365
- String twoDigits(int value) => value.toString().padLeft(2, '0');
366
- String removeCRandLF(String input) => input.replaceAll(RegExp('\r|\n'), '');
367
- await logFile.writeAsString('${twoDigits(date.hour)}:${twoDigits(date.minute)}:${twoDigits(date.second)} ${removeCRandLF(logMessage)}\n',
368
- mode: FileMode.append);
369
- }*/
370
-
371
- static void info(String s) => _logInfo ? debugPrint('[info] $s') : null;
372
- static void error(String s) => _logError ? debugPrint('[error] $s') : null;
373
- static void printWrapped(String text) => RegExp('.{1,800}').allMatches(text).map((m) => m.group(0)).forEach(print);
374
- static void _json(String methodName, String json) => _logJson ? printWrapped('$methodName $json') : null;
375
-
360
}
361
362
Future<String> callSyncMethod(String methodName, int hWallet, String params) async {