mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-06-28 12:29:51 +00:00
* CW-934 Implement passphrase creation for zano * Update monero_c dependency to latest commit Fix issue with zano keys not showing during sync Fix delays when invoking read only commands in zano Fix extra padding above passphrase Reduced lag during app use
511 lines
No EOL
19 KiB
Dart
511 lines
No EOL
19 KiB
Dart
import 'dart:convert' as convert;
|
|
import 'dart:ffi';
|
|
import 'dart:io';
|
|
import 'dart:isolate';
|
|
|
|
import 'package:cw_core/transaction_priority.dart';
|
|
import 'package:cw_core/utils/print_verbose.dart';
|
|
import 'package:cw_core/zano_asset.dart';
|
|
import 'package:cw_zano/api/consts.dart';
|
|
import 'package:cw_zano/api/model/asset_id_params.dart';
|
|
import 'package:cw_zano/api/model/create_wallet_result.dart';
|
|
import 'package:cw_zano/api/model/destination.dart';
|
|
import 'package:cw_zano/api/model/get_address_info_result.dart';
|
|
import 'package:cw_zano/api/model/get_recent_txs_and_info_params.dart';
|
|
import 'package:cw_zano/api/model/get_recent_txs_and_info_result.dart';
|
|
import 'package:cw_zano/api/model/get_wallet_info_result.dart';
|
|
import 'package:cw_zano/api/model/get_wallet_status_result.dart';
|
|
import 'package:cw_zano/api/model/proxy_to_daemon_params.dart';
|
|
import 'package:cw_zano/api/model/proxy_to_daemon_result.dart';
|
|
import 'package:cw_zano/api/model/store_result.dart';
|
|
import 'package:cw_zano/api/model/transfer.dart';
|
|
import 'package:cw_zano/api/model/transfer_params.dart';
|
|
import 'package:cw_zano/api/model/transfer_result.dart';
|
|
import 'package:cw_zano/zano_wallet_exceptions.dart';
|
|
import 'package:ffi/ffi.dart';
|
|
import 'package:json_bigint/json_bigint.dart';
|
|
import 'package:monero/zano.dart' as zano;
|
|
import 'package:monero/src/generated_bindings_zano.g.dart' as zanoapi;
|
|
import 'package:path/path.dart' as p;
|
|
|
|
mixin ZanoWalletApi {
|
|
static const _maxReopenAttempts = 5;
|
|
static const _logInfo = false;
|
|
static const int _zanoMixinValue = 10;
|
|
|
|
int _hWallet = 0;
|
|
|
|
int get hWallet => _hWallet;
|
|
|
|
set hWallet(int value) {
|
|
_hWallet = value;
|
|
}
|
|
|
|
int getCurrentTxFee(TransactionPriority priority) => zano.PlainWallet_getCurrentTxFee(priority.raw);
|
|
|
|
void setPassword(String password) => zano.PlainWallet_resetWalletPassword(hWallet, password);
|
|
|
|
void closeWallet(int? walletToClose, {bool force = false}) async {
|
|
printV('close_wallet ${walletToClose ?? hWallet}: $force');
|
|
if (Platform.isWindows || force) {
|
|
final result = await _closeWallet(walletToClose ?? hWallet);
|
|
printV('close_wallet result $result');
|
|
openWalletCache.removeWhere((_, cwr) => cwr.walletId == (walletToClose ?? hWallet));
|
|
}
|
|
}
|
|
|
|
static bool isInit = false;
|
|
|
|
Future<bool> initWallet() async {
|
|
if (isInit) return true;
|
|
final result = zano.PlainWallet_init("", "", 0);
|
|
isInit = true;
|
|
return result == "OK";
|
|
}
|
|
|
|
Future<bool> setupNode(String nodeUrl) async {
|
|
await _setupNode(hWallet, nodeUrl);
|
|
return true;
|
|
}
|
|
|
|
Future<Directory> getWalletDir() async {
|
|
final walletInfoResult = await getWalletInfo();
|
|
return Directory(p.dirname(walletInfoResult.wi.path));
|
|
}
|
|
|
|
Future<File> _getWalletSecretsFile() async {
|
|
final dir = await getWalletDir();
|
|
final file = File(p.join(dir.path, "zano-secrets.json.bin"));
|
|
return file;
|
|
}
|
|
|
|
Future<Map<String, dynamic>> _getSecrets() async {
|
|
final file = await _getWalletSecretsFile();
|
|
if (!file.existsSync()) {
|
|
return {};
|
|
}
|
|
final data = file.readAsBytesSync();
|
|
final b64 = convert.base64.encode(data);
|
|
final respStr = await invokeMethod("decrypt_data", {"buff": "$b64"});
|
|
final resp = convert.json.decode(respStr);
|
|
final dataBytes = convert.base64.decode(resp["result"]["res_buff"] as String);
|
|
final dataStr = convert.utf8.decode(dataBytes);
|
|
final dataObject = convert.json.decode(dataStr);
|
|
return dataObject as Map<String, dynamic>;
|
|
}
|
|
|
|
Future<void> _setSecrets(Map<String, dynamic> data) async {
|
|
final dataStr = convert.json.encode(data);
|
|
final b64 = convert.base64.encode(convert.utf8.encode(dataStr));
|
|
final respStr = await invokeMethod("encrypt_data", {"buff": "$b64"});
|
|
final resp = convert.json.decode(respStr);
|
|
final dataBytes = convert.base64.decode(resp["result"]["res_buff"] as String);
|
|
final file = await _getWalletSecretsFile();
|
|
file.writeAsBytesSync(dataBytes);
|
|
}
|
|
|
|
Future<String?> _getWalletSecret(String key) async {
|
|
final secrets = await _getSecrets();
|
|
return secrets[key] as String?;
|
|
}
|
|
|
|
Future<void> _setWalletSecret(String key, String value) async {
|
|
final secrets = await _getSecrets();
|
|
secrets[key] = value;
|
|
await _setSecrets(secrets);
|
|
}
|
|
|
|
Future<String?> getPassphrase() async {
|
|
return await _getWalletSecret("passphrase");
|
|
}
|
|
|
|
Future<void> setPassphrase(String passphrase) {
|
|
return _setWalletSecret("passphrase", passphrase);
|
|
}
|
|
|
|
Future<String> getSeed() async {
|
|
final passphrase = await getPassphrase();
|
|
final respStr = await invokeMethod("get_restore_info", {"seed_password": passphrase??""});
|
|
final resp = convert.json.decode(respStr);
|
|
return resp["result"]["seed_phrase"] as String;
|
|
}
|
|
|
|
Future<GetWalletInfoResult> getWalletInfo() async {
|
|
final json = await _getWalletInfo(hWallet);
|
|
final result = GetWalletInfoResult.fromJson(jsonDecode(json));
|
|
printV('get_wallet_info got ${result.wi.balances.length} balances: ${result.wi.balances}');
|
|
return result;
|
|
}
|
|
|
|
Future<GetWalletStatusResult> getWalletStatus() async {
|
|
final json = await _getWalletStatus(hWallet);
|
|
if (json == Consts.errorWalletWrongId) {
|
|
printV('wrong wallet id');
|
|
throw ZanoWalletException('Wrong wallet id');
|
|
}
|
|
final status = GetWalletStatusResult.fromJson(jsonDecode(json));
|
|
if (_logInfo)
|
|
printV(
|
|
'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)}%');
|
|
return status;
|
|
}
|
|
|
|
Future<String> invokeMethod(String methodName, Object params) async {
|
|
final request = jsonEncode({
|
|
"method": methodName,
|
|
"params": params,
|
|
});
|
|
final invokeResult = await callSyncMethod('invoke', hWallet, request);
|
|
try {
|
|
jsonDecode(invokeResult);
|
|
} catch (e) {
|
|
if (invokeResult.contains(Consts.errorWalletWrongId)) throw ZanoWalletException('Wrong wallet id');
|
|
printV('exception in parsing json in invokeMethod: $invokeResult');
|
|
rethrow;
|
|
}
|
|
return invokeResult;
|
|
}
|
|
|
|
Future<List<ZanoAsset>> getAssetsWhitelist() async {
|
|
try {
|
|
final json = await invokeMethod('assets_whitelist_get', '{}');
|
|
final map = jsonDecode(json) as Map<String, dynamic>?;
|
|
_checkForErrors(map);
|
|
List<ZanoAsset> assets(String type, bool isGlobalWhitelist) =>
|
|
(map?['result']?[type] as List<dynamic>?)
|
|
?.map((e) => ZanoAsset.fromJson(e as Map<String, dynamic>, isInGlobalWhitelist: isGlobalWhitelist))
|
|
.toList() ??
|
|
[];
|
|
final localWhitelist = assets('local_whitelist', false);
|
|
final globalWhitelist = assets('global_whitelist', true);
|
|
final ownAssets = assets('own_assets', false);
|
|
if (_logInfo)
|
|
printV('assets_whitelist_get got local whitelist: ${localWhitelist.length} ($localWhitelist); '
|
|
'global whitelist: ${globalWhitelist.length} ($globalWhitelist); '
|
|
'own assets: ${ownAssets.length} ($ownAssets)');
|
|
return [...globalWhitelist, ...localWhitelist, ...ownAssets];
|
|
} catch (e) {
|
|
printV('assets_whitelist_get $e');
|
|
return [];
|
|
// rethrow;
|
|
}
|
|
}
|
|
|
|
Future<ZanoAsset?> addAssetsWhitelist(String assetId) async {
|
|
try {
|
|
final json = await invokeMethod('assets_whitelist_add', AssetIdParams(assetId: assetId));
|
|
final map = jsonDecode(json) as Map<String, dynamic>?;
|
|
_checkForErrors(map);
|
|
if (map!['result']!['status']! == 'OK') {
|
|
final assetDescriptor = ZanoAsset.fromJson(map['result']!['asset_descriptor']! as Map<String, dynamic>);
|
|
printV('assets_whitelist_add added ${assetDescriptor.fullName} ${assetDescriptor.ticker}');
|
|
return assetDescriptor;
|
|
} else {
|
|
printV('assets_whitelist_add status ${map['result']!['status']!}');
|
|
return null;
|
|
}
|
|
} catch (e) {
|
|
printV('assets_whitelist_add $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<bool> removeAssetsWhitelist(String assetId) async {
|
|
try {
|
|
final json = await invokeMethod('assets_whitelist_remove', AssetIdParams(assetId: assetId));
|
|
final map = jsonDecode(json) as Map<String, dynamic>?;
|
|
_checkForErrors(map);
|
|
printV('assets_whitelist_remove status ${map!['result']!['status']!}');
|
|
return (map['result']!['status']! == 'OK');
|
|
} catch (e) {
|
|
printV('assets_whitelist_remove $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<ProxyToDaemonResult?> _proxyToDaemon(String uri, String body) async {
|
|
final json = await invokeMethod('proxy_to_daemon', ProxyToDaemonParams(body: body, uri: uri));
|
|
final map = jsonDecode(json) as Map<String, dynamic>?;
|
|
_checkForErrors(map);
|
|
return ProxyToDaemonResult.fromJson(map!['result'] as Map<String, dynamic>);
|
|
}
|
|
|
|
Future<ZanoAsset?> getAssetInfo(String assetId) async {
|
|
final methodName = 'get_asset_info';
|
|
final params = AssetIdParams(assetId: assetId);
|
|
final result = await _proxyToDaemon('/json_rpc', '{"method": "$methodName","params": ${jsonEncode(params)}}');
|
|
if (result == null) {
|
|
printV('get_asset_info empty result');
|
|
return null;
|
|
}
|
|
final map = jsonDecode(result.body) as Map<String, dynamic>?;
|
|
if (map!['error'] != null) {
|
|
printV('get_asset_info $assetId error ${map['error']!['code']} ${map['error']!['message']}');
|
|
return null;
|
|
} else if (map['result']!['status']! == 'OK') {
|
|
final assetDescriptor = ZanoAsset.fromJson(map['result']!['asset_descriptor']! as Map<String, dynamic>);
|
|
printV('get_asset_info $assetId ${assetDescriptor.fullName} ${assetDescriptor.ticker}');
|
|
return assetDescriptor;
|
|
} else {
|
|
printV('get_asset_info $assetId status ${map['result']!['status']!}');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<StoreResult?> store() async {
|
|
try {
|
|
final json = await invokeMethod('store', {});
|
|
final map = jsonDecode(json) as Map<String, dynamic>?;
|
|
_checkForErrors(map);
|
|
return StoreResult.fromJson(map!['result'] as Map<String, dynamic>);
|
|
} catch (e) {
|
|
printV('store $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<GetRecentTxsAndInfoResult> getRecentTxsAndInfo({required int offset, required int count}) async {
|
|
printV('get_recent_txs_and_info $offset $count');
|
|
try {
|
|
final json = await invokeMethod('get_recent_txs_and_info', GetRecentTxsAndInfoParams(offset: offset, count: count));
|
|
final map = jsonDecode(json) as Map<String, dynamic>?;
|
|
_checkForErrors(map);
|
|
final lastItemIndex = map?['result']?['last_item_index'] as int?;
|
|
final totalTransfers = map?['result']?['total_transfers'] as int?;
|
|
final transfers = map?['result']?['transfers'] as List<dynamic>?;
|
|
if (transfers == null || lastItemIndex == null || totalTransfers == null) {
|
|
printV('get_recent_txs_and_info empty transfers');
|
|
return GetRecentTxsAndInfoResult.empty();
|
|
}
|
|
printV('get_recent_txs_and_info transfers.length: ${transfers.length}');
|
|
return GetRecentTxsAndInfoResult(
|
|
transfers: transfers.map((e) => Transfer.fromJson(e as Map<String, dynamic>)).toList(),
|
|
lastItemIndex: lastItemIndex,
|
|
totalTransfers: totalTransfers,
|
|
);
|
|
} catch (e) {
|
|
printV('get_recent_txs_and_info $e');
|
|
return GetRecentTxsAndInfoResult.empty();
|
|
}
|
|
}
|
|
|
|
GetAddressInfoResult getAddressInfo(String address) => GetAddressInfoResult.fromJson(
|
|
jsonDecode(zano.PlainWallet_getAddressInfo(address)),
|
|
);
|
|
|
|
String _shorten(String s) => s.length > 10 ? '${s.substring(0, 4)}...${s.substring(s.length - 4)}' : s;
|
|
|
|
Future<CreateWalletResult> createWallet(String path, String password) async {
|
|
printV('create_wallet path $path password ${_shorten(password)}');
|
|
final json = zano.PlainWallet_generate(path, password);
|
|
final map = jsonDecode(json) as Map<String, dynamic>?;
|
|
if (map?['error'] != null) {
|
|
final code = map!['error']?['code'] ?? '';
|
|
final message = map['error']?['message'] ?? '';
|
|
throw ZanoWalletException('Error creating wallet file, $message ($code)');
|
|
}
|
|
if (map?['result'] == null) {
|
|
throw ZanoWalletException('Error creating wallet file, empty response');
|
|
}
|
|
final result = CreateWalletResult.fromJson(map!['result'] as Map<String, dynamic>);
|
|
openWalletCache[path] = result;
|
|
printV('create_wallet ${result.name}');
|
|
return result;
|
|
}
|
|
|
|
Future<CreateWalletResult> restoreWalletFromSeed(String path, String password, String seed, String? passphrase) async {
|
|
printV('restore_wallet path $path');
|
|
final json = zano.PlainWallet_restore(seed, path, password, passphrase??'');
|
|
final map = jsonDecode(json) as Map<String, dynamic>?;
|
|
if (map?['error'] != null) {
|
|
final code = map!['error']!['code'] ?? '';
|
|
final message = map['error']!['message'] ?? '';
|
|
if (code == Consts.errorWrongSeed) {
|
|
throw RestoreFromSeedsException('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.');
|
|
} else if (code == Consts.errorAlreadyExists) {
|
|
throw RestoreFromSeedsException('Error restoring wallet, already exists');
|
|
}
|
|
throw RestoreFromSeedsException('Error restoring wallet, $message ($code)');
|
|
}
|
|
if (map?['result'] == null) {
|
|
throw RestoreFromSeedsException('Error restoring wallet, empty response');
|
|
}
|
|
final result = CreateWalletResult.fromJson(map!['result'] as Map<String, dynamic>);
|
|
openWalletCache[path] = result;
|
|
printV('restore_wallet ${result.name} ${result.wi.address}');
|
|
return result;
|
|
}
|
|
|
|
Future<CreateWalletResult> loadWallet(String path, String password, [int attempt = 0]) async {
|
|
printV('load_wallet1 path $path');
|
|
final String json;
|
|
try {
|
|
json = zano.PlainWallet_open(path, password);
|
|
} catch (e) {
|
|
printV('error in loadingWallet $e');
|
|
rethrow;
|
|
}
|
|
|
|
final map = jsonDecode(json) as Map<String, dynamic>?;
|
|
if (map?['error'] != null) {
|
|
final code = map?['error']!['code'] ?? '';
|
|
final message = map?['error']!['message'] ?? '';
|
|
if (code == Consts.errorAlreadyExists && attempt <= _maxReopenAttempts) {
|
|
// already connected to this wallet. closing and trying to reopen
|
|
printV('already connected. closing and reopen wallet (attempt $attempt)');
|
|
closeWallet(attempt, force: true);
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
return await loadWallet(path, password, attempt + 1);
|
|
}
|
|
throw ZanoWalletException('Error loading wallet, $message ($code)');
|
|
}
|
|
if (map?['result'] == null) {
|
|
throw ZanoWalletException('Error loading wallet, empty response');
|
|
}
|
|
final result = CreateWalletResult.fromJson(map!['result'] as Map<String, dynamic>);
|
|
printV('load_wallet3 ${result.name} ${result.wi.address}');
|
|
openWalletCache[path] = result;
|
|
return result;
|
|
}
|
|
|
|
static Map<String, CreateWalletResult> openWalletCache = {};
|
|
|
|
Future<TransferResult> transfer(List<Destination> destinations, BigInt fee, String comment) async {
|
|
final params = TransferParams(
|
|
destinations: destinations,
|
|
fee: fee,
|
|
mixin: _zanoMixinValue,
|
|
paymentId: '',
|
|
comment: comment,
|
|
pushPayer: false,
|
|
hideReceiver: true,
|
|
);
|
|
final json = await invokeMethod('transfer', params);
|
|
final map = jsonDecode(json);
|
|
final resultMap = map as Map<String, dynamic>?;
|
|
if (resultMap != null) {
|
|
final transferResultMap = resultMap['result'] as Map<String, dynamic>?;
|
|
if (transferResultMap != null) {
|
|
final transferResult = TransferResult.fromJson(transferResultMap);
|
|
printV('transfer success hash ${transferResult.txHash}');
|
|
return transferResult;
|
|
} else {
|
|
final errorCode = resultMap['error']?['code'];
|
|
final code = errorCode is int ? errorCode.toString() : errorCode as String? ?? '';
|
|
final message = resultMap['error']?['message'] as String? ?? '';
|
|
printV('transfer error $code $message');
|
|
throw TransferException('Transfer error, $message ($code)');
|
|
}
|
|
}
|
|
printV('transfer error empty result');
|
|
throw TransferException('Transfer error, empty result');
|
|
}
|
|
|
|
void _checkForErrors(Map<String, dynamic>? map) {
|
|
if (map == null) {
|
|
throw ZanoWalletException('Empty response');
|
|
}
|
|
final result = map['result'];
|
|
if (result == null) {
|
|
throw ZanoWalletException('Empty response');
|
|
}
|
|
if (result['error'] != null) {
|
|
final code = result['error']!['code'] ?? '';
|
|
final message = result['error']!['message'] ?? '';
|
|
if (code == -1 && message == Consts.errorBusy) {
|
|
throw ZanoWalletBusyException();
|
|
}
|
|
throw ZanoWalletException('Error, $message ($code)');
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
Future<String> callSyncMethod(String methodName, int hWallet, String params) async {
|
|
final params_ = params.toNativeUtf8().address;
|
|
final method_name_ = methodName.toNativeUtf8().address;
|
|
final invokeResult = await Isolate.run(() async {
|
|
final lib = zanoapi.ZanoC(DynamicLibrary.open(zano.libPath));
|
|
final txid = lib.ZANO_PlainWallet_syncCall(
|
|
Pointer.fromAddress(method_name_).cast(),
|
|
hWallet,
|
|
Pointer.fromAddress(params_).cast()
|
|
);
|
|
try {
|
|
final strPtr = txid.cast<Utf8>();
|
|
final str = strPtr.toDartString();
|
|
lib.ZANO_free(strPtr.cast());
|
|
return str;
|
|
} catch (e) {
|
|
return "";
|
|
}
|
|
});
|
|
calloc.free(Pointer.fromAddress(method_name_));
|
|
calloc.free(Pointer.fromAddress(params_));
|
|
return invokeResult;
|
|
}
|
|
|
|
Map<String, dynamic> jsonDecode(String json) {
|
|
try {
|
|
return decodeJson(json.replaceAll("\\/", "/")) as Map<String, dynamic>;
|
|
} catch (e) {
|
|
return convert.jsonDecode(json) as Map<String, dynamic>;
|
|
}
|
|
}
|
|
|
|
String jsonEncode(Object? object) {
|
|
return convert.jsonEncode(object);
|
|
}
|
|
|
|
Future<String> _getWalletStatus(int hWallet) async {
|
|
final jsonPtr = await Isolate.run(() async {
|
|
final lib = zanoapi.ZanoC(DynamicLibrary.open(zano.libPath));
|
|
final status = lib.ZANO_PlainWallet_getWalletStatus(
|
|
hWallet,
|
|
);
|
|
return status.address;
|
|
});
|
|
String json = "";
|
|
try {
|
|
final strPtr = Pointer.fromAddress(jsonPtr).cast<Utf8>();
|
|
final str = strPtr.toDartString();
|
|
zano.ZANO_free(strPtr.cast());
|
|
json = str;
|
|
} catch (e) {
|
|
json = "";
|
|
}
|
|
return json;
|
|
}
|
|
Future<String> _getWalletInfo(int hWallet) async {
|
|
final jsonPtr = await Isolate.run(() async {
|
|
final lib = zanoapi.ZanoC(DynamicLibrary.open(zano.libPath));
|
|
final status = lib.ZANO_PlainWallet_getWalletInfo(
|
|
hWallet,
|
|
);
|
|
return status.address;
|
|
});
|
|
String json = "";
|
|
try {
|
|
final strPtr = Pointer.fromAddress(jsonPtr).cast<Utf8>();
|
|
final str = strPtr.toDartString();
|
|
zano.ZANO_free(strPtr.cast());
|
|
json = str;
|
|
} catch (e) {
|
|
json = "";
|
|
}
|
|
return json;
|
|
}
|
|
|
|
Future<String> _setupNode(int hWallet, String nodeUrl) async {
|
|
await callSyncMethod("reset_connection_url", hWallet, nodeUrl);
|
|
await callSyncMethod("run_wallet", hWallet, "");
|
|
return "OK";
|
|
}
|
|
|
|
Future<String> _closeWallet(int hWallet) async {
|
|
final str = await Isolate.run(() async {
|
|
return zano.PlainWallet_closeWallet(hWallet);
|
|
});
|
|
printV("Closing wallet: $str");
|
|
return str;
|
|
} |