mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-06-28 12:29:51 +00:00
v4.23.0 release candidate (#1974)
* v4.23.0 release candidate * - Fix restoring zano from QR - Fix Zano confirmations count - Fix birdpay - Fix balance display * Fix Zano assets showing amount before they are added * - handle fetching token data while the API is busy - potential fix for duplicate transactions * fix receive confirmations, maybe * revert onChangeWallet cleanup * Fix confirmations not updating * improve zano wallet opening, fix CI commands and messages on slack (#1979) Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * Cache wallet when creating/restoring as well * - hardcode Trocador Maximum limit for Zano temporarily - Configure Cake Zano node to use SSL * reformatting [skip ci] * revert to non-ssl * update build numbers [skip ci] * disable zano for desktop [skip ci] --------- Co-authored-by: cyan <cyjan@mrcyjanek.net>
This commit is contained in:
parent
aef90e7192
commit
141a7ebfca
42 changed files with 472 additions and 306 deletions
|
@ -1,12 +1,10 @@
|
|||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/zano_asset.dart';
|
||||
import 'package:cw_zano/api/model/employed_entries.dart';
|
||||
import 'package:cw_zano/api/model/subtransfer.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cw_zano/model/zano_asset.dart';
|
||||
import 'package:cw_zano/model/zano_transaction_info.dart';
|
||||
import 'package:cw_zano/zano_formatter.dart';
|
||||
import 'package:cw_zano/zano_wallet.dart';
|
||||
import 'package:cw_zano/zano_wallet_api.dart';
|
||||
|
||||
class Transfer {
|
||||
final String comment;
|
||||
|
@ -49,78 +47,87 @@ class Transfer {
|
|||
required this.unlockTime,
|
||||
});
|
||||
|
||||
factory Transfer.fromJson(Map<String, dynamic> json) => Transfer(
|
||||
factory Transfer.fromJson(Map<String, dynamic> json) =>
|
||||
Transfer(
|
||||
comment: json['comment'] as String? ?? '',
|
||||
employedEntries: EmployedEntries.fromJson(json['employed_entries'] as Map<String, dynamic>? ?? {}),
|
||||
employedEntries: EmployedEntries.fromJson(
|
||||
json['employed_entries'] as Map<String, dynamic>? ?? {}),
|
||||
fee: json['fee'] as int? ?? 0,
|
||||
height: json['height'] as int? ?? 0,
|
||||
isMining: json['is_mining'] as bool? ?? false,
|
||||
isMixing: json['is_mixing'] as bool? ?? false,
|
||||
isService: json['is_service'] as bool? ?? false,
|
||||
paymentId: json['payment_id'] as String? ?? '',
|
||||
remoteAddresses: json['remote_addresses'] == null ? [] : (json['remote_addresses'] as List<dynamic>).cast<String>(),
|
||||
remoteAliases: json['remote_aliases'] == null ? [] : (json['remote_aliases'] as List<dynamic>).cast<String>(),
|
||||
remoteAddresses: json['remote_addresses'] == null ? [] : (json['remote_addresses'] as List<
|
||||
dynamic>).cast<String>(),
|
||||
remoteAliases: json['remote_aliases'] == null ? [] : (json['remote_aliases'] as List<
|
||||
dynamic>).cast<String>(),
|
||||
showSender: json['show_sender'] as bool? ?? false,
|
||||
subtransfers: (json['subtransfers'] as List<dynamic>? ?? []).map((e) => Subtransfer.fromJson(e as Map<String, dynamic>)).toList(),
|
||||
subtransfers: (json['subtransfers'] as List<dynamic>? ?? []).map((e) =>
|
||||
Subtransfer.fromJson(e as Map<String, dynamic>)).toList(),
|
||||
timestamp: json['timestamp'] as int? ?? 0,
|
||||
transferInternalIndex: json['transfer_internal_index'] == null
|
||||
? 0
|
||||
: json['transfer_internal_index'] is double
|
||||
? (json['transfer_internal_index'] as double).toInt()
|
||||
: json['transfer_internal_index'] as int,
|
||||
? (json['transfer_internal_index'] as double).toInt()
|
||||
: json['transfer_internal_index'] as int,
|
||||
txBlobSize: json['tx_blob_size'] as int? ?? 0,
|
||||
txHash: json['tx_hash'] as String? ?? '',
|
||||
txType: json['tx_type'] as int? ?? 0,
|
||||
unlockTime: json['unlock_time'] as int? ?? 0,
|
||||
);
|
||||
|
||||
static Map<String, ZanoTransactionInfo> makeMap(List<Transfer> transfers, Map<String, ZanoAsset> zanoAssets, int currentDaemonHeight) {
|
||||
static Map<String, ZanoTransactionInfo> makeMap(List<Transfer> transfers,
|
||||
Map<String, ZanoAsset> zanoAssets, int currentDaemonHeight) {
|
||||
return Map.fromIterable(
|
||||
transfers,
|
||||
key: (item) => (item as Transfer).txHash,
|
||||
value: (transfer) {
|
||||
transfer as Transfer;
|
||||
// Simple (only one subtransfer OR two subtransfers and the second is Zano, outgoing and amount equals to fee) or complex?
|
||||
Subtransfer? single = transfer.subtransfers.singleOrNull;
|
||||
if (transfer.subtransfers.length == 2) {
|
||||
final zano = transfer.subtransfers.firstWhereOrNull((element) => element.assetId == ZanoWalletBase.zanoAssetId);
|
||||
if (zano != null && !zano.isIncome && zano.amount == BigInt.from(transfer.fee)) {
|
||||
single = transfer.subtransfers.firstWhere((element) => element.assetId != ZanoWalletBase.zanoAssetId);
|
||||
}
|
||||
transfers,
|
||||
key: (item) => (item as Transfer).txHash,
|
||||
value: (transfer) {
|
||||
transfer as Transfer;
|
||||
// Simple (only one subtransfer OR two subtransfers and the second is Zano, outgoing and amount equals to fee) or complex?
|
||||
Subtransfer? single = transfer.subtransfers.singleOrNull;
|
||||
if (transfer.subtransfers.length == 2) {
|
||||
final zano = transfer.subtransfers.firstWhereOrNull((element) =>
|
||||
element.assetId == ZanoWalletBase.zanoAssetId);
|
||||
if (zano != null && !zano.isIncome && zano.amount == BigInt.from(transfer.fee)) {
|
||||
single = transfer.subtransfers.firstWhere((element) => element.assetId !=
|
||||
ZanoWalletBase.zanoAssetId);
|
||||
}
|
||||
bool isSimple = single != null;
|
||||
// TODO: for complex transactions we show zano or any other transaction, will fix it later
|
||||
if (!isSimple) {
|
||||
single =
|
||||
transfer.subtransfers.firstWhereOrNull((element) => element.assetId == ZanoWalletBase.zanoAssetId) ?? transfer.subtransfers.first;
|
||||
}
|
||||
bool isSimple = single != null;
|
||||
// TODO: for complex transactions we show zano or any other transaction, will fix it later
|
||||
if (!isSimple) {
|
||||
single =
|
||||
transfer.subtransfers.firstWhereOrNull((element) =>
|
||||
element.assetId == ZanoWalletBase.zanoAssetId) ?? transfer.subtransfers.first;
|
||||
}
|
||||
if (single.assetId != ZanoWalletBase.zanoAssetId) {
|
||||
final asset = zanoAssets[single.assetId];
|
||||
if (asset == null) {
|
||||
printV('unknown asset ${single.assetId}');
|
||||
}
|
||||
if (single.assetId != ZanoWalletBase.zanoAssetId) {
|
||||
final asset = zanoAssets[single.assetId];
|
||||
if (asset == null) {
|
||||
ZanoWalletApi.error('unknown asset ${single.assetId}');
|
||||
}
|
||||
final ticker = asset == null ? '***' : asset.ticker;
|
||||
final decimalPoint = asset == null ? ZanoFormatter.defaultDecimalPoint : asset.decimalPoint;
|
||||
return ZanoTransactionInfo.fromTransfer(
|
||||
transfer,
|
||||
confirmations: currentDaemonHeight - transfer.height,
|
||||
isIncome: single.isIncome,
|
||||
assetId: single.assetId,
|
||||
amount: single.amount,
|
||||
tokenSymbol: isSimple ? ticker : '*${ticker}',
|
||||
decimalPoint: decimalPoint,
|
||||
);
|
||||
}
|
||||
final amount = single.isIncome ? single.amount : single.amount - BigInt.from(transfer.fee);
|
||||
final ticker = asset == null ? '***' : asset.ticker;
|
||||
final decimalPoint = asset == null ? 0 : asset.decimalPoint;
|
||||
return ZanoTransactionInfo.fromTransfer(
|
||||
transfer,
|
||||
confirmations: currentDaemonHeight - transfer.height,
|
||||
isIncome: single.isIncome,
|
||||
assetId: single.assetId,
|
||||
amount: amount,
|
||||
tokenSymbol: isSimple ? 'ZANO' : '*ZANO',
|
||||
amount: single.amount,
|
||||
tokenSymbol: isSimple ? ticker : '*${ticker}',
|
||||
decimalPoint: decimalPoint,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
final amount = single.isIncome ? single.amount : single.amount - BigInt.from(transfer.fee);
|
||||
return ZanoTransactionInfo.fromTransfer(
|
||||
transfer,
|
||||
confirmations: currentDaemonHeight - transfer.height,
|
||||
isIncome: single.isIncome,
|
||||
assetId: single.assetId,
|
||||
amount: amount,
|
||||
tokenSymbol: isSimple ? 'ZANO' : '*ZANO',
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,10 +32,10 @@ class PendingZanoTransaction with PendingTransaction {
|
|||
String get hex => '';
|
||||
|
||||
@override
|
||||
String get amountFormatted => '${ZanoFormatter.bigIntAmountToString(amount, decimalPoint)} $ticker';
|
||||
String get amountFormatted => ZanoFormatter.bigIntAmountToString(amount, decimalPoint);
|
||||
|
||||
@override
|
||||
String get feeFormatted => '${ZanoFormatter.bigIntAmountToString(fee)} ZANO';
|
||||
String get feeFormatted => ZanoFormatter.bigIntAmountToString(fee);
|
||||
|
||||
TransferResult? transferResult;
|
||||
|
||||
|
|
|
@ -23,11 +23,11 @@ class ZanoTransactionInfo extends TransactionInfo {
|
|||
|
||||
ZanoTransactionInfo.fromTransfer(Transfer transfer,
|
||||
{required int confirmations,
|
||||
required bool isIncome,
|
||||
required String assetId,
|
||||
required BigInt amount,
|
||||
this.tokenSymbol = 'ZANO',
|
||||
this.decimalPoint = ZanoFormatter.defaultDecimalPoint})
|
||||
required bool isIncome,
|
||||
required String assetId,
|
||||
required BigInt amount,
|
||||
this.tokenSymbol = 'ZANO',
|
||||
this.decimalPoint = ZanoFormatter.defaultDecimalPoint})
|
||||
: id = transfer.txHash,
|
||||
height = transfer.height,
|
||||
direction = isIncome ? TransactionDirection.incoming : TransactionDirection.outgoing,
|
||||
|
@ -36,14 +36,18 @@ class ZanoTransactionInfo extends TransactionInfo {
|
|||
amount = amount.isValidInt ? amount.toInt() : 0,
|
||||
fee = transfer.fee,
|
||||
confirmations = confirmations,
|
||||
isPending = false,
|
||||
recipientAddress = transfer.remoteAddresses.isNotEmpty ? transfer.remoteAddresses.first : '' {
|
||||
isPending = confirmations < 10,
|
||||
recipientAddress = transfer.remoteAddresses.isNotEmpty
|
||||
? transfer.remoteAddresses.first
|
||||
: '' {
|
||||
additionalInfo = <String, dynamic>{
|
||||
'comment': transfer.comment,
|
||||
'assetId': assetId,
|
||||
};
|
||||
}
|
||||
|
||||
String get assetId => additionalInfo["assetId"] as String;
|
||||
|
||||
set assetId(String newId) => additionalInfo["assetId"] = newId;
|
||||
final String id;
|
||||
final int height;
|
||||
|
@ -61,7 +65,8 @@ class ZanoTransactionInfo extends TransactionInfo {
|
|||
String? key;
|
||||
|
||||
@override
|
||||
String amountFormatted() => '${formatAmount(ZanoFormatter.bigIntAmountToString(zanoAmount, decimalPoint))} $tokenSymbol';
|
||||
String amountFormatted() =>
|
||||
'${formatAmount(ZanoFormatter.bigIntAmountToString(zanoAmount, decimalPoint))} $tokenSymbol';
|
||||
|
||||
@override
|
||||
String fiatAmount() => _fiatAmount ?? '';
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_zano/zano_wallet_api.dart';
|
||||
import 'package:decimal/decimal.dart';
|
||||
import 'package:decimal/intl.dart';
|
||||
|
@ -32,6 +33,9 @@ class ZanoFormatter {
|
|||
}
|
||||
|
||||
static String bigIntAmountToString(BigInt amount, [int decimalPoint = defaultDecimalPoint]) {
|
||||
if (decimalPoint == 0) {
|
||||
return '0';
|
||||
}
|
||||
final numberFormat = NumberFormat()..maximumFractionDigits = decimalPoint
|
||||
..minimumFractionDigits = 1;
|
||||
return numberFormat.format(
|
||||
|
@ -66,7 +70,7 @@ class ZanoFormatter {
|
|||
} else if (d == null) {
|
||||
return BigInt.zero;
|
||||
} else {
|
||||
ZanoWalletApi.error('cannot cast value of type ${d.runtimeType} to BigInt');
|
||||
printV(('cannot cast value of type ${d.runtimeType} to BigInt'));
|
||||
throw 'cannot cast value of type ${d.runtimeType} to BigInt';
|
||||
//return BigInt.zero;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:core';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
|
@ -43,7 +44,7 @@ abstract class ZanoWalletBase
|
|||
extends WalletBase<ZanoBalance, ZanoTransactionHistory, ZanoTransactionInfo>
|
||||
with Store, ZanoWalletApi {
|
||||
static const int _autoSaveIntervalSeconds = 30;
|
||||
static const int _pollIntervalMilliseconds = 2000;
|
||||
static const int _pollIntervalMilliseconds = 5000;
|
||||
static const int _maxLoadAssetsRetries = 5;
|
||||
|
||||
@override
|
||||
|
@ -152,19 +153,27 @@ abstract class ZanoWalletBase
|
|||
static Future<ZanoWallet> open(
|
||||
{required String name, required String password, required WalletInfo walletInfo}) async {
|
||||
final path = await pathForWallet(name: name, type: walletInfo.type);
|
||||
final wallet = ZanoWallet(walletInfo, password);
|
||||
await wallet.initWallet();
|
||||
final createWalletResult = await wallet.loadWallet(path, password);
|
||||
await wallet.initWallet();
|
||||
await wallet.parseCreateWalletResult(createWalletResult);
|
||||
await wallet.init(createWalletResult.wi.address);
|
||||
return wallet;
|
||||
if (ZanoWalletApi.openWalletCache[path] != null) {
|
||||
final wallet = ZanoWallet(walletInfo, password);
|
||||
await wallet.parseCreateWalletResult(ZanoWalletApi.openWalletCache[path]!).then((_) {
|
||||
unawaited(wallet.init(ZanoWalletApi.openWalletCache[path]!.wi.address));
|
||||
});
|
||||
return wallet;
|
||||
} else {
|
||||
final wallet = ZanoWallet(walletInfo, password);
|
||||
await wallet.initWallet();
|
||||
final createWalletResult = await wallet.loadWallet(path, password);
|
||||
await wallet.parseCreateWalletResult(createWalletResult).then((_) {
|
||||
unawaited(wallet.init(createWalletResult.wi.address));
|
||||
});
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> parseCreateWalletResult(CreateWalletResult result) async {
|
||||
hWallet = result.walletId;
|
||||
seed = result.seed;
|
||||
ZanoWalletApi.info('setting hWallet = ${result.walletId}');
|
||||
printV('setting hWallet = ${result.walletId}');
|
||||
walletAddresses.address = result.wi.address;
|
||||
await loadAssets(result.wi.balances, maxRetries: _maxLoadAssetsRetries);
|
||||
for (final item in result.wi.balances) {
|
||||
|
@ -185,7 +194,7 @@ abstract class ZanoWalletBase
|
|||
|
||||
@override
|
||||
Future<void> close({bool shouldCleanup = true}) async {
|
||||
closeWallet();
|
||||
closeWallet(null);
|
||||
_updateSyncInfoTimer?.cancel();
|
||||
_autoSaveTimer?.cancel();
|
||||
}
|
||||
|
@ -285,7 +294,7 @@ abstract class ZanoWalletBase
|
|||
} while (result.lastItemIndex + 1 < result.totalTransfers);
|
||||
return Transfer.makeMap(transfers, zanoAssets, currentDaemonHeight);
|
||||
} catch (e) {
|
||||
ZanoWalletApi.error(e.toString());
|
||||
printV((e.toString()));
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
@ -332,7 +341,7 @@ abstract class ZanoWalletBase
|
|||
await store();
|
||||
await walletAddresses.updateAddressesInBox();
|
||||
} catch (e) {
|
||||
ZanoWalletApi.error('Error while saving Zano wallet file ${e.toString()}');
|
||||
printV(('Error while saving Zano wallet file ${e.toString()}'));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -349,7 +358,7 @@ abstract class ZanoWalletBase
|
|||
retryCount++;
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
} else {
|
||||
ZanoWalletApi.error('failed to load assets after $retryCount retries');
|
||||
printV(('failed to load assets after $retryCount retries'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -371,65 +380,10 @@ abstract class ZanoWalletBase
|
|||
_lastKnownBlockHeight = 0;
|
||||
_initialSyncHeight = 0;
|
||||
_updateSyncInfoTimer ??=
|
||||
Timer.periodic(Duration(milliseconds: _pollIntervalMilliseconds), (_) async {
|
||||
GetWalletStatusResult walletStatus;
|
||||
// ignoring get wallet status exception (in case of wrong wallet id)
|
||||
try {
|
||||
walletStatus = await getWalletStatus();
|
||||
} on ZanoWalletException {
|
||||
return;
|
||||
}
|
||||
currentDaemonHeight = walletStatus.currentDaemonHeight;
|
||||
_updateSyncProgress(walletStatus);
|
||||
|
||||
// we can call getWalletInfo ONLY if getWalletStatus returns NOT is in long refresh and wallet state is 2 (ready)
|
||||
if (!walletStatus.isInLongRefresh && walletStatus.walletState == 2) {
|
||||
final walletInfo = await getWalletInfo();
|
||||
seed = walletInfo.wiExtended.seed;
|
||||
keys = ZanoWalletKeys(
|
||||
privateSpendKey: walletInfo.wiExtended.spendPrivateKey,
|
||||
privateViewKey: walletInfo.wiExtended.viewPrivateKey,
|
||||
publicSpendKey: walletInfo.wiExtended.spendPublicKey,
|
||||
publicViewKey: walletInfo.wiExtended.viewPublicKey,
|
||||
);
|
||||
loadAssets(walletInfo.wi.balances);
|
||||
// matching balances and whitelists
|
||||
// 1. show only balances available in whitelists
|
||||
// 2. set whitelists available in balances as 'enabled' ('disabled' by default)
|
||||
for (final b in walletInfo.wi.balances) {
|
||||
if (b.assetId == zanoAssetId) {
|
||||
balance[CryptoCurrency.zano] = ZanoBalance(total: b.total, unlocked: b.unlocked);
|
||||
} else {
|
||||
final asset = zanoAssets[b.assetId];
|
||||
if (asset == null) {
|
||||
ZanoWalletApi.error('balance for an unknown asset ${b.assetInfo.assetId}');
|
||||
continue;
|
||||
}
|
||||
if (balance.keys.any(
|
||||
(element) => element is ZanoAsset && element.assetId == b.assetInfo.assetId)) {
|
||||
balance[balance.keys.firstWhere((element) =>
|
||||
element is ZanoAsset && element.assetId == b.assetInfo.assetId)] =
|
||||
ZanoBalance(
|
||||
total: b.total, unlocked: b.unlocked, decimalPoint: asset.decimalPoint);
|
||||
} else {
|
||||
balance[asset] = ZanoBalance(
|
||||
total: b.total, unlocked: b.unlocked, decimalPoint: asset.decimalPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
await updateTransactions();
|
||||
// removing balances for assets missing in wallet info balances
|
||||
balance.removeWhere(
|
||||
(key, _) =>
|
||||
key != CryptoCurrency.zano &&
|
||||
!walletInfo.wi.balances
|
||||
.any((element) => element.assetId == (key as ZanoAsset).assetId),
|
||||
);
|
||||
}
|
||||
});
|
||||
Timer.periodic(Duration(milliseconds: _pollIntervalMilliseconds), (_) => _updateSyncInfo());
|
||||
} catch (e) {
|
||||
syncStatus = FailedSyncStatus();
|
||||
ZanoWalletApi.error(e.toString());
|
||||
printV((e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -443,17 +397,13 @@ abstract class ZanoWalletBase
|
|||
}
|
||||
_isTransactionUpdating = true;
|
||||
final transactions = await fetchTransactions();
|
||||
if (transactions.length == transactionHistory.transactions.length) {
|
||||
_isTransactionUpdating = false;
|
||||
return;
|
||||
}
|
||||
transactionHistory.clear();
|
||||
transactionHistory.addMany(transactions);
|
||||
await transactionHistory.save();
|
||||
_isTransactionUpdating = false;
|
||||
} catch (e) {
|
||||
printV("e: $e");
|
||||
ZanoWalletApi.error(e.toString());
|
||||
printV((e.toString()));
|
||||
_isTransactionUpdating = false;
|
||||
}
|
||||
}
|
||||
|
@ -480,12 +430,12 @@ abstract class ZanoWalletBase
|
|||
if (asset.enabled) {
|
||||
final assetDescriptor = await addAssetsWhitelist(asset.assetId);
|
||||
if (assetDescriptor == null) {
|
||||
ZanoWalletApi.error('Error adding zano asset');
|
||||
printV(('Error adding zano asset'));
|
||||
}
|
||||
} else {
|
||||
final result = await removeAssetsWhitelist(asset.assetId);
|
||||
if (result == false) {
|
||||
ZanoWalletApi.error('Error removing zano asset');
|
||||
printV(('Error removing zano asset'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -495,7 +445,11 @@ abstract class ZanoWalletBase
|
|||
}
|
||||
|
||||
Future<ZanoAsset?> getZanoAsset(String assetId) async {
|
||||
return await getAssetInfo(assetId);
|
||||
// wallet api is not available while the wallet is syncing so only call it if it's synced
|
||||
if (syncStatus is SyncedSyncStatus) {
|
||||
return await getAssetInfo(assetId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _askForUpdateTransactionHistory() async => await updateTransactions();
|
||||
|
@ -514,7 +468,7 @@ abstract class ZanoWalletBase
|
|||
syncStatus = SyncingSyncStatus(blocksLeft, ptc);
|
||||
}
|
||||
} catch (e) {
|
||||
ZanoWalletApi.error(e.toString());
|
||||
printV((e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -542,4 +496,61 @@ abstract class ZanoWalletBase
|
|||
// 1. Actual new height; 2. Blocks left to finish; 3. Progress in percents;
|
||||
_onNewBlock.call(syncHeight, left, ptc);
|
||||
}
|
||||
|
||||
void _updateSyncInfo() async {
|
||||
GetWalletStatusResult walletStatus;
|
||||
// ignoring get wallet status exception (in case of wrong wallet id)
|
||||
try {
|
||||
walletStatus = await getWalletStatus();
|
||||
} on ZanoWalletException {
|
||||
return;
|
||||
}
|
||||
currentDaemonHeight = walletStatus.currentDaemonHeight;
|
||||
_updateSyncProgress(walletStatus);
|
||||
|
||||
// we can call getWalletInfo ONLY if getWalletStatus returns NOT is in long refresh and wallet state is 2 (ready)
|
||||
if (!walletStatus.isInLongRefresh && walletStatus.walletState == 2) {
|
||||
final walletInfo = await getWalletInfo();
|
||||
seed = walletInfo.wiExtended.seed;
|
||||
keys = ZanoWalletKeys(
|
||||
privateSpendKey: walletInfo.wiExtended.spendPrivateKey,
|
||||
privateViewKey: walletInfo.wiExtended.viewPrivateKey,
|
||||
publicSpendKey: walletInfo.wiExtended.spendPublicKey,
|
||||
publicViewKey: walletInfo.wiExtended.viewPublicKey,
|
||||
);
|
||||
loadAssets(walletInfo.wi.balances);
|
||||
// matching balances and whitelists
|
||||
// 1. show only balances available in whitelists
|
||||
// 2. set whitelists available in balances as 'enabled' ('disabled' by default)
|
||||
for (final b in walletInfo.wi.balances) {
|
||||
if (b.assetId == zanoAssetId) {
|
||||
balance[CryptoCurrency.zano] = ZanoBalance(total: b.total, unlocked: b.unlocked);
|
||||
} else {
|
||||
final asset = zanoAssets[b.assetId];
|
||||
if (asset == null) {
|
||||
printV('balance for an unknown asset ${b.assetInfo.assetId}');
|
||||
continue;
|
||||
}
|
||||
if (balance.keys.any(
|
||||
(element) => element is ZanoAsset && element.assetId == b.assetInfo.assetId)) {
|
||||
balance[balance.keys.firstWhere((element) =>
|
||||
element is ZanoAsset && element.assetId == b.assetInfo.assetId)] =
|
||||
ZanoBalance(
|
||||
total: b.total, unlocked: b.unlocked, decimalPoint: asset.decimalPoint);
|
||||
} else {
|
||||
balance[asset] = ZanoBalance(
|
||||
total: b.total, unlocked: b.unlocked, decimalPoint: asset.decimalPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
await updateTransactions();
|
||||
// removing balances for assets missing in wallet info balances
|
||||
balance.removeWhere(
|
||||
(key, _) =>
|
||||
key != CryptoCurrency.zano &&
|
||||
!walletInfo.wi.balances
|
||||
.any((element) => element.assetId == (key as ZanoAsset).assetId),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_addresses.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_zano/zano_wallet_api.dart';
|
||||
|
@ -34,7 +35,7 @@ abstract class ZanoWalletAddressesBase extends WalletAddresses with Store {
|
|||
addressesMap[address] = '';
|
||||
await saveAddressesInBox();
|
||||
} catch (e) {
|
||||
ZanoWalletApi.error(e.toString());
|
||||
printV(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:convert' as convert;
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
|
@ -22,7 +23,6 @@ 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:flutter/foundation.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;
|
||||
|
@ -30,8 +30,6 @@ import 'package:monero/src/generated_bindings_zano.g.dart' as zanoapi;
|
|||
mixin ZanoWalletApi {
|
||||
static const _maxReopenAttempts = 5;
|
||||
static const _logInfo = false;
|
||||
static const _logError = true;
|
||||
static const _logJson = false;
|
||||
static const int _zanoMixinValue = 10;
|
||||
|
||||
int _hWallet = 0;
|
||||
|
@ -46,16 +44,22 @@ mixin ZanoWalletApi {
|
|||
|
||||
void setPassword(String password) => zano.PlainWallet_resetWalletPassword(hWallet, password);
|
||||
|
||||
void closeWallet([int? walletToClose]) async {
|
||||
info('close_wallet ${walletToClose ?? hWallet}');
|
||||
final result = await _closeWallet(walletToClose ?? hWallet);
|
||||
info('close_wallet result $result');
|
||||
void closeWallet(int? walletToClose, {bool force = false}) async {
|
||||
printV('close_wallet ${walletToClose ?? hWallet}');
|
||||
if (Platform.isWindows || force) {
|
||||
final result = await _closeWallet(walletToClose ?? hWallet);
|
||||
printV('close_wallet result $result');
|
||||
openWalletCache.removeWhere((_, cwr) => cwr.walletId == (walletToClose ?? hWallet));
|
||||
}
|
||||
}
|
||||
|
||||
bool isInit = false;
|
||||
|
||||
Future<bool> initWallet() async {
|
||||
// pathForWallet(name: , type: type)
|
||||
if (isInit) return true;
|
||||
final result = zano.PlainWallet_init("", "", 0);
|
||||
printV(result);
|
||||
isInit = true;
|
||||
return result == "OK";
|
||||
}
|
||||
|
||||
|
@ -67,21 +71,19 @@ mixin ZanoWalletApi {
|
|||
Future<GetWalletInfoResult> getWalletInfo() async {
|
||||
final json = await _getWalletInfo(hWallet);
|
||||
final result = GetWalletInfoResult.fromJson(jsonDecode(json));
|
||||
_json('get_wallet_info', json);
|
||||
info('get_wallet_info got ${result.wi.balances.length} balances: ${result.wi.balances} seed: ${_shorten(result.wiExtended.seed)}');
|
||||
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) {
|
||||
error('wrong wallet id');
|
||||
printV('wrong wallet id');
|
||||
throw ZanoWalletException('Wrong wallet id');
|
||||
}
|
||||
final status = GetWalletStatusResult.fromJson(jsonDecode(json));
|
||||
_json('get_wallet_status', json);
|
||||
if (_logInfo)
|
||||
info(
|
||||
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;
|
||||
}
|
||||
|
@ -96,7 +98,7 @@ mixin ZanoWalletApi {
|
|||
jsonDecode(invokeResult);
|
||||
} catch (e) {
|
||||
if (invokeResult.contains(Consts.errorWalletWrongId)) throw ZanoWalletException('Wrong wallet id');
|
||||
error('exception in parsing json in invokeMethod: $invokeResult');
|
||||
printV('exception in parsing json in invokeMethod: $invokeResult');
|
||||
rethrow;
|
||||
}
|
||||
return invokeResult;
|
||||
|
@ -105,7 +107,6 @@ mixin ZanoWalletApi {
|
|||
Future<List<ZanoAsset>> getAssetsWhitelist() async {
|
||||
try {
|
||||
final json = await invokeMethod('assets_whitelist_get', '{}');
|
||||
_json('assets_whitelist_get', json);
|
||||
final map = jsonDecode(json) as Map<String, dynamic>?;
|
||||
_checkForErrors(map);
|
||||
List<ZanoAsset> assets(String type, bool isGlobalWhitelist) =>
|
||||
|
@ -117,12 +118,12 @@ mixin ZanoWalletApi {
|
|||
final globalWhitelist = assets('global_whitelist', true);
|
||||
final ownAssets = assets('own_assets', false);
|
||||
if (_logInfo)
|
||||
info('assets_whitelist_get got local whitelist: ${localWhitelist.length} ($localWhitelist); '
|
||||
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) {
|
||||
error('assets_whitelist_get $e');
|
||||
printV('assets_whitelist_get $e');
|
||||
return [];
|
||||
// rethrow;
|
||||
}
|
||||
|
@ -131,19 +132,18 @@ mixin ZanoWalletApi {
|
|||
Future<ZanoAsset?> addAssetsWhitelist(String assetId) async {
|
||||
try {
|
||||
final json = await invokeMethod('assets_whitelist_add', AssetIdParams(assetId: assetId));
|
||||
_json('assets_whitelist_add $assetId', json);
|
||||
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>);
|
||||
info('assets_whitelist_add added ${assetDescriptor.fullName} ${assetDescriptor.ticker}');
|
||||
printV('assets_whitelist_add added ${assetDescriptor.fullName} ${assetDescriptor.ticker}');
|
||||
return assetDescriptor;
|
||||
} else {
|
||||
info('assets_whitelist_add status ${map['result']!['status']!}');
|
||||
printV('assets_whitelist_add status ${map['result']!['status']!}');
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
error('assets_whitelist_add $e');
|
||||
printV('assets_whitelist_add $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -151,13 +151,12 @@ mixin ZanoWalletApi {
|
|||
Future<bool> removeAssetsWhitelist(String assetId) async {
|
||||
try {
|
||||
final json = await invokeMethod('assets_whitelist_remove', AssetIdParams(assetId: assetId));
|
||||
_json('assets_whitelist_remove $assetId', json);
|
||||
final map = jsonDecode(json) as Map<String, dynamic>?;
|
||||
_checkForErrors(map);
|
||||
info('assets_whitelist_remove status ${map!['result']!['status']!}');
|
||||
printV('assets_whitelist_remove status ${map!['result']!['status']!}');
|
||||
return (map['result']!['status']! == 'OK');
|
||||
} catch (e) {
|
||||
error('assets_whitelist_remove $e');
|
||||
printV('assets_whitelist_remove $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -173,21 +172,20 @@ mixin ZanoWalletApi {
|
|||
final methodName = 'get_asset_info';
|
||||
final params = AssetIdParams(assetId: assetId);
|
||||
final result = await _proxyToDaemon('/json_rpc', '{"method": "$methodName","params": ${jsonEncode(params)}}');
|
||||
_json('$methodName $assetId', result?.body ?? '');
|
||||
if (result == null) {
|
||||
error('get_asset_info empty result');
|
||||
printV('get_asset_info empty result');
|
||||
return null;
|
||||
}
|
||||
final map = jsonDecode(result.body) as Map<String, dynamic>?;
|
||||
if (map!['error'] != null) {
|
||||
info('get_asset_info $assetId error ${map['error']!['code']} ${map['error']!['message']}');
|
||||
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>);
|
||||
info('get_asset_info $assetId ${assetDescriptor.fullName} ${assetDescriptor.ticker}');
|
||||
printV('get_asset_info $assetId ${assetDescriptor.fullName} ${assetDescriptor.ticker}');
|
||||
return assetDescriptor;
|
||||
} else {
|
||||
info('get_asset_info $assetId status ${map['result']!['status']!}');
|
||||
printV('get_asset_info $assetId status ${map['result']!['status']!}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -199,33 +197,32 @@ mixin ZanoWalletApi {
|
|||
_checkForErrors(map);
|
||||
return StoreResult.fromJson(map!['result'] as Map<String, dynamic>);
|
||||
} catch (e) {
|
||||
error('store $e');
|
||||
printV('store $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<GetRecentTxsAndInfoResult> getRecentTxsAndInfo({required int offset, required int count}) async {
|
||||
info('get_recent_txs_and_info $offset $count');
|
||||
printV('get_recent_txs_and_info $offset $count');
|
||||
try {
|
||||
final json = await invokeMethod('get_recent_txs_and_info', GetRecentTxsAndInfoParams(offset: offset, count: count));
|
||||
_json('get_recent_txs_and_info', json);
|
||||
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) {
|
||||
error('get_recent_txs_and_info empty transfers');
|
||||
printV('get_recent_txs_and_info empty transfers');
|
||||
return GetRecentTxsAndInfoResult.empty();
|
||||
}
|
||||
info('get_recent_txs_and_info transfers.length: ${transfers.length}');
|
||||
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) {
|
||||
error('get_recent_txs_and_info $e');
|
||||
printV('get_recent_txs_and_info $e');
|
||||
return GetRecentTxsAndInfoResult.empty();
|
||||
}
|
||||
}
|
||||
|
@ -237,9 +234,8 @@ mixin ZanoWalletApi {
|
|||
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 {
|
||||
info('create_wallet path $path password ${_shorten(password)}');
|
||||
printV('create_wallet path $path password ${_shorten(password)}');
|
||||
final json = zano.PlainWallet_generate(path, password);
|
||||
_json('create_wallet', json);
|
||||
final map = jsonDecode(json) as Map<String, dynamic>?;
|
||||
if (map?['error'] != null) {
|
||||
final code = map!['error']?['code'] ?? '';
|
||||
|
@ -250,14 +246,14 @@ mixin ZanoWalletApi {
|
|||
throw ZanoWalletException('Error creating wallet file, empty response');
|
||||
}
|
||||
final result = CreateWalletResult.fromJson(map!['result'] as Map<String, dynamic>);
|
||||
info('create_wallet ${result.name} ${result.seed}');
|
||||
openWalletCache[path] = result;
|
||||
printV('create_wallet ${result.name} ${result.seed}');
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<CreateWalletResult> restoreWalletFromSeed(String path, String password, String seed, String? passphrase) async {
|
||||
info('restore_wallet path $path password ${_shorten(password)} seed ${_shorten(seed)}');
|
||||
printV('restore_wallet path $path password ${_shorten(password)} seed ${_shorten(seed)}');
|
||||
final json = zano.PlainWallet_restore(seed, path, password, passphrase??'');
|
||||
_json('restore_wallet', json);
|
||||
final map = jsonDecode(json) as Map<String, dynamic>?;
|
||||
if (map?['error'] != null) {
|
||||
final code = map!['error']!['code'] ?? '';
|
||||
|
@ -273,28 +269,29 @@ mixin ZanoWalletApi {
|
|||
throw RestoreFromSeedsException('Error restoring wallet, empty response');
|
||||
}
|
||||
final result = CreateWalletResult.fromJson(map!['result'] as Map<String, dynamic>);
|
||||
info('restore_wallet ${result.name} ${result.wi.address}');
|
||||
openWalletCache[path] = result;
|
||||
printV('restore_wallet ${result.name} ${result.wi.address}');
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<CreateWalletResult>loadWallet(String path, String password, [int attempt = 0]) async {
|
||||
info('load_wallet1 path $path password ${_shorten(password)}');
|
||||
printV('load_wallet1 path $path password ${_shorten(password)}');
|
||||
final String json;
|
||||
try {
|
||||
json = zano.PlainWallet_open(path, password);
|
||||
} catch (e) {
|
||||
error('error in loadingWallet $e');
|
||||
printV('error in loadingWallet $e');
|
||||
rethrow;
|
||||
}
|
||||
info('load_wallet2: $json');
|
||||
// printV('load_wallet2: $json');
|
||||
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
|
||||
info('already connected. closing and reopen wallet (attempt $attempt)');
|
||||
closeWallet(attempt);
|
||||
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);
|
||||
}
|
||||
|
@ -304,10 +301,13 @@ mixin ZanoWalletApi {
|
|||
throw ZanoWalletException('Error loading wallet, empty response');
|
||||
}
|
||||
final result = CreateWalletResult.fromJson(map!['result'] as Map<String, dynamic>);
|
||||
info('load_wallet3 ${result.name} ${result.wi.address}');
|
||||
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,
|
||||
|
@ -319,24 +319,23 @@ mixin ZanoWalletApi {
|
|||
hideReceiver: true,
|
||||
);
|
||||
final json = await invokeMethod('transfer', params);
|
||||
_json('transfer', json);
|
||||
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);
|
||||
info('transfer success hash ${transferResult.txHash}');
|
||||
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? ?? '';
|
||||
error('transfer error $code $message');
|
||||
printV('transfer error $code $message');
|
||||
throw TransferException('Transfer error, $message ($code)');
|
||||
}
|
||||
}
|
||||
error('transfer error empty result');
|
||||
printV('transfer error empty result');
|
||||
throw TransferException('Transfer error, empty result');
|
||||
}
|
||||
|
||||
|
@ -358,21 +357,6 @@ mixin ZanoWalletApi {
|
|||
}
|
||||
}
|
||||
|
||||
/*Future<void> _writeLog(String method, String logMessage) async {
|
||||
final dir = await getDownloadsDirectory();
|
||||
final logFile = File('${dir!.path}/$method.txt');
|
||||
final date = DateTime.now();
|
||||
String twoDigits(int value) => value.toString().padLeft(2, '0');
|
||||
String removeCRandLF(String input) => input.replaceAll(RegExp('\r|\n'), '');
|
||||
await logFile.writeAsString('${twoDigits(date.hour)}:${twoDigits(date.minute)}:${twoDigits(date.second)} ${removeCRandLF(logMessage)}\n',
|
||||
mode: FileMode.append);
|
||||
}*/
|
||||
|
||||
static void info(String s) => _logInfo ? debugPrint('[info] $s') : null;
|
||||
static void error(String s) => _logError ? debugPrint('[error] $s') : null;
|
||||
static void printWrapped(String text) => RegExp('.{1,800}').allMatches(text).map((m) => m.group(0)).forEach(print);
|
||||
static void _json(String methodName, String json) => _logJson ? printWrapped('$methodName $json') : null;
|
||||
|
||||
}
|
||||
|
||||
Future<String> callSyncMethod(String methodName, int hWallet, String params) async {
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:io';
|
|||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:cw_core/pathForWallet.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_credentials.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
|
@ -55,7 +56,7 @@ class ZanoWalletService extends WalletService<ZanoNewWalletCredentials,
|
|||
|
||||
@override
|
||||
Future<ZanoWallet> create(WalletCredentials credentials, {bool? isTestnet}) async {
|
||||
ZanoWalletApi.info('zanowallet service create isTestnet $isTestnet');
|
||||
printV('zanowallet service create isTestnet $isTestnet');
|
||||
return await ZanoWalletBase.create(credentials: credentials);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue