cake_wallet/cw_zano/lib/zano_wallet.dart

504 lines
17 KiB
Dart
Raw Normal View History

2023-10-02 14:17:35 +00:00
import 'dart:async';
import 'dart:io';
2023-12-14 04:51:16 +00:00
import 'package:cw_core/cake_hive.dart';
2023-10-02 14:17:35 +00:00
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/monero_amount_format.dart';
2023-12-14 04:51:16 +00:00
import 'package:cw_core/monero_wallet_utils.dart';
import 'package:cw_core/node.dart';
2023-10-02 14:17:35 +00:00
import 'package:cw_core/pathForWallet.dart';
2023-12-14 04:51:16 +00:00
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
2023-10-02 14:17:35 +00:00
import 'package:cw_core/transaction_priority.dart';
2023-12-14 04:51:16 +00:00
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
2024-03-06 06:48:59 +00:00
import 'package:cw_zano/api/api_calls.dart';
import 'package:cw_zano/api/model/destination.dart';
import 'package:cw_zano/api/model/get_wallet_status_result.dart';
2023-12-14 04:51:16 +00:00
import 'package:cw_zano/api/model/history.dart';
import 'package:cw_zano/api/model/zano_wallet_keys.dart';
import 'package:cw_zano/exceptions/zano_transaction_creation_exception.dart';
2023-10-02 14:17:35 +00:00
import 'package:cw_zano/pending_zano_transaction.dart';
import 'package:cw_zano/zano_asset.dart';
2023-10-02 14:17:35 +00:00
import 'package:cw_zano/zano_balance.dart';
import 'package:cw_zano/zano_transaction_credentials.dart';
2023-12-14 04:51:16 +00:00
import 'package:cw_zano/zano_transaction_history.dart';
import 'package:cw_zano/zano_transaction_info.dart';
import 'package:cw_zano/zano_wallet_addresses.dart';
2024-03-15 12:42:27 +00:00
import 'package:cw_zano/zano_wallet_api.dart';
2023-12-16 12:19:11 +00:00
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
2023-12-14 04:51:16 +00:00
import 'package:mobx/mobx.dart';
2023-10-02 14:17:35 +00:00
import 'default_zano_assets.dart';
2023-10-02 14:17:35 +00:00
part 'zano_wallet.g.dart';
2024-03-15 12:42:27 +00:00
const int zanoMixinValue = 10;
2023-10-02 14:17:35 +00:00
class ZanoWallet = ZanoWalletBase with _$ZanoWallet;
2023-12-14 04:51:16 +00:00
2024-03-15 12:42:27 +00:00
abstract class ZanoWalletBase extends WalletBase<ZanoBalance, ZanoTransactionHistory, ZanoTransactionInfo> with Store, ZanoWalletApi {
2023-10-02 14:17:35 +00:00
static const int _autoSaveInterval = 30;
List<History> history = [];
2024-03-15 12:42:27 +00:00
//String defaultAsssetId = '';
2023-10-02 14:17:35 +00:00
@override
ZanoWalletAddresses walletAddresses;
@override
@observable
SyncStatus syncStatus;
@override
@observable
2023-11-17 17:40:23 +00:00
ObservableMap<CryptoCurrency, ZanoBalance> balance;
2023-10-02 14:17:35 +00:00
@override
2023-12-14 04:51:16 +00:00
String seed = '';
2023-10-02 14:17:35 +00:00
@override
ZanoWalletKeys keys = ZanoWalletKeys(privateSpendKey: '', privateViewKey: '', publicSpendKey: '', publicViewKey: '');
late final Box<ZanoAsset> zanoAssetsBox;
List<ZanoAsset> get zanoAssets => zanoAssetsBox.values.toList();
2023-10-02 14:17:35 +00:00
//zano_wallet.SyncListener? _listener;
// ReactionDisposer? _onAccountChangeReaction;
Timer? _updateSyncInfoTimer;
int _cachedBlockchainHeight = 0;
int _lastKnownBlockHeight = 0;
int _initialSyncHeight = 0;
2023-10-02 14:17:35 +00:00
bool _isTransactionUpdating;
bool _hasSyncAfterStartup;
Timer? _autoSaveTimer;
2023-11-17 17:40:23 +00:00
ZanoWalletBase(WalletInfo walletInfo)
: balance = ObservableMap.of({CryptoCurrency.zano: ZanoBalance(total: 0, unlocked: 0)}),
_isTransactionUpdating = false,
_hasSyncAfterStartup = false,
walletAddresses = ZanoWalletAddresses(walletInfo),
syncStatus = NotConnectedSyncStatus(),
super(walletInfo) {
transactionHistory = ZanoTransactionHistory();
2024-03-15 12:42:27 +00:00
if (!CakeHive.isAdapterRegistered(ZanoAsset.typeId)) {
CakeHive.registerAdapter(ZanoAssetAdapter());
}
// _onAccountChangeReaction =
// reaction((_) => walletAddresses.account, (Account? account) {
// if (account == null) {
// return;
// }
// balance.addAll(getZanoBalance(accountIndex: account.id));
// /**walletAddresses.updateSubaddressList(accountIndex: account.id);*/
// });
}
@override
2024-03-15 12:42:27 +00:00
int calculateEstimatedFee(TransactionPriority priority, [int? amount = null]) => getCurrentTxFee(priority);
2023-10-02 14:17:35 +00:00
@override
Future<void> changePassword(String password) async {
2024-03-15 12:42:27 +00:00
setPassword(password);
}
2023-10-02 14:17:35 +00:00
@override
void close() {
2024-03-15 12:42:27 +00:00
closeWallet();
_updateSyncInfoTimer?.cancel();
//_listener?.stop();
// _onAccountChangeReaction?.reaction.dispose();
2023-10-02 14:17:35 +00:00
_autoSaveTimer?.cancel();
}
@override
Future<void> connectToNode({required Node node}) async {
2024-03-15 12:42:27 +00:00
syncStatus = ConnectingSyncStatus();
await setupNode();
syncStatus = ConnectedSyncStatus();
// if (await setupNode() == false) {
// syncStatus = FailedSyncStatus();
// // TODO: what's going on?
// //throw 'error connecting to zano node';
// } else {
// syncStatus = ConnectedSyncStatus();
// }
2023-10-02 14:17:35 +00:00
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
final creds = credentials as ZanoTransactionCredentials;
final outputs = creds.outputs;
2023-10-02 14:17:35 +00:00
final hasMultiDestination = outputs.length > 1;
final unlockedBalance = balance[CryptoCurrency.zano]?.unlocked ?? 0;
final fee = calculateEstimatedFee(creds.priority);
late List<Destination> destinations;
2023-10-02 14:17:35 +00:00
if (hasMultiDestination) {
if (outputs.any((output) => output.sendAll || (output.formattedCryptoAmount ?? 0) <= 0)) {
throw ZanoTransactionCreationException("You don't have enough coins.");
2023-10-02 14:17:35 +00:00
}
final int totalAmount = outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0));
if (totalAmount + fee > unlockedBalance) {
2023-10-02 14:17:35 +00:00
throw ZanoTransactionCreationException(
"You don't have enough coins (required: ${moneroAmountToString(amount: totalAmount + fee)}, unlocked ${moneroAmountToString(amount: unlockedBalance)}).");
2023-10-02 14:17:35 +00:00
}
destinations = outputs
.map((output) => Destination(
amount: output.formattedCryptoAmount ?? 0,
address: output.isParsedAddress ? output.extractedAddress! : output.address,
2024-03-15 12:42:27 +00:00
assetId: "defaultAsssetId",
))
2023-10-02 14:17:35 +00:00
.toList();
} else {
final output = outputs.first;
late int amount;
if (output.sendAll) {
amount = unlockedBalance - fee;
} else {
amount = output.formattedCryptoAmount!;
}
if (amount + fee > unlockedBalance) {
2023-10-02 14:17:35 +00:00
throw ZanoTransactionCreationException(
"You don't have enough coins (required: ${moneroAmountToString(amount: amount + fee)}, unlocked ${moneroAmountToString(amount: unlockedBalance)}).");
2023-10-02 14:17:35 +00:00
}
destinations = [
Destination(
amount: amount,
address: output.isParsedAddress ? output.extractedAddress! : output.address,
2024-03-15 12:42:27 +00:00
assetId: "defaultAsssetId",
)
];
2023-10-02 14:17:35 +00:00
}
2024-03-08 12:42:09 +00:00
destinations.forEach((destination) {
debugPrint('destination ${destination.address} ${destination.amount} ${destination.assetId}');
2024-03-08 12:42:09 +00:00
});
return PendingZanoTransaction(
zanoWallet: this,
destinations: destinations,
fee: fee,
comment: outputs.first.note ?? '',
);
2023-10-02 14:17:35 +00:00
}
@override
Future<Map<String, ZanoTransactionInfo>> fetchTransactions() async {
2024-03-06 06:48:59 +00:00
try {
await _refreshTransactions();
return history.map<ZanoTransactionInfo>((history) => ZanoTransactionInfo.fromHistory(history)).fold<Map<String, ZanoTransactionInfo>>(
<String, ZanoTransactionInfo>{},
(Map<String, ZanoTransactionInfo> acc, ZanoTransactionInfo tx) {
acc[tx.id] = tx;
return acc;
},
);
2024-03-06 06:48:59 +00:00
} catch (e) {
print(e);
return {};
2024-03-06 06:48:59 +00:00
}
}
Future<void> init(String address) async {
2024-03-15 12:42:27 +00:00
final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${ZanoAsset.zanoAssetsBoxName}";
zanoAssetsBox = await CakeHive.openBox<ZanoAsset>(boxName);
print(
'assets in box total: ${zanoAssetsBox.length} ${zanoAssetsBox.values} active: ${zanoAssetsBox.values.where((element) => element.enabled).length} ${zanoAssetsBox.values.where((element) => element.enabled)}');
for (final asset in zanoAssetsBox.values) {
if (asset.enabled) balance[asset] = ZanoBalance(total: 0, unlocked: 0);
}
await walletAddresses.init();
await walletAddresses.updateAddress(address);
///balance.addAll(getZanoBalance(/**accountIndex: walletAddresses.account?.id ?? 0*/));
//_setListeners();
await updateTransactions();
_autoSaveTimer = Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save());
}
String loadWallet(String path, String password) {
print('load_wallet path $path password $password');
final result = ApiCalls.loadWallet(path: path, password: password);
print('load_wallet result $result');
return result;
2023-10-02 14:17:35 +00:00
}
@override
Future<void> renameWalletFiles(String newWalletName) async {
final currentWalletPath = await pathForWallet(name: name, type: type);
final currentCacheFile = File(currentWalletPath);
final currentKeysFile = File('$currentWalletPath.keys');
final currentAddressListFile = File('$currentWalletPath.address.txt');
final newWalletPath = await pathForWallet(name: newWalletName, type: type);
// Copies current wallet files into new wallet name's dir and files
if (currentCacheFile.existsSync()) {
await currentCacheFile.copy(newWalletPath);
}
if (currentKeysFile.existsSync()) {
await currentKeysFile.copy('$newWalletPath.keys');
}
if (currentAddressListFile.existsSync()) {
await currentAddressListFile.copy('$newWalletPath.address.txt');
}
// Delete old name's dir and files
await Directory(currentWalletPath).delete(recursive: true);
}
@override
Future<void> rescan({required int height}) async {
walletInfo.restoreHeight = height;
walletInfo.isRecovery = true;
2023-12-16 12:19:11 +00:00
debugPrint('setRefreshFromBlockHeight height $height');
debugPrint('rescanBlockchainAsync');
2023-10-02 14:17:35 +00:00
await startSync();
2023-11-17 17:40:23 +00:00
/**walletAddresses.accountList.update();*/
2023-10-02 14:17:35 +00:00
await _askForUpdateTransactionHistory();
await save();
await walletInfo.save();
}
@override
Future<void> save() async {
2024-03-06 06:48:59 +00:00
try {
await walletAddresses.updateAddressesInBox();
await backupWalletFiles(name);
await store();
} catch (e) {
print('Error while saving Zano wallet file ${e.toString()}');
}
}
2024-03-06 06:48:59 +00:00
Future<void> setAsRecovered() async {
walletInfo.isRecovery = false;
await walletInfo.save();
}
2024-03-06 06:48:59 +00:00
2024-03-15 12:42:27 +00:00
bool _calledOnce = false;
int _counter = 0;
@override
Future<void> startSync() async {
try {
syncStatus = AttemptingSyncStatus();
_cachedBlockchainHeight = 0;
_lastKnownBlockHeight = 0;
_initialSyncHeight = 0;
2024-03-15 12:42:27 +00:00
_updateSyncInfoTimer ??= Timer.periodic(Duration(milliseconds: /*1200*/5000), (_) async {
/*if (isNewTransactionExist()) {
onNewTransaction?.call();
}*/
2024-03-06 06:48:59 +00:00
final walletStatus = getWalletStatus();
_updateSyncProgress(walletStatus);
// You 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 = getWalletInfo();
seed = walletInfo.wiExtended.seed;
keys = ZanoWalletKeys(
privateSpendKey: walletInfo.wiExtended.spendPrivateKey,
privateViewKey: walletInfo.wiExtended.viewPrivateKey,
publicSpendKey: walletInfo.wiExtended.spendPublicKey,
publicViewKey: walletInfo.wiExtended.viewPublicKey,
);
2024-03-06 06:48:59 +00:00
2024-03-15 12:42:27 +00:00
for (final item in walletInfo.wi.balances) {
if (item.assetInfo.ticker == 'ZANO') {
balance[CryptoCurrency.zano] = ZanoBalance(total: item.total, unlocked: item.unlocked);
} else {
for (final asset in balance.keys) {
if (asset is ZanoAsset && asset.assetId == item.assetInfo.assetId) {
balance[asset] = ZanoBalance(total: item.total, unlocked: item.unlocked);
}
}
}
}
//await getAssetsWhitelist();
if (!_calledOnce) {
//await addAssetsWhitelist('00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff');
//await removeAssetsWhitelist('cc4e69455e63f4a581257382191de6856c2156630b3fba0db4bdd73ffcfb36b6');
//await removeAssetsWhitelist('bb9590162509f956ff79851fb1bc0ced6646f5d5ba7eae847a9f21c92c39437c');
//await removeAssetsWhitelist('');
_calledOnce = true;
} else {
await getAssetsWhitelist();
}
// if (++_counter >= 10) {
// await getAssetsWhitelist();
// _counter = 0;
// }
}
});
2024-03-06 06:48:59 +00:00
} catch (e) {
syncStatus = FailedSyncStatus();
print(e);
rethrow;
2023-12-14 04:51:16 +00:00
}
}
2023-10-02 14:17:35 +00:00
@override
Future<void>? updateBalance() => null;
2023-10-02 14:17:35 +00:00
Future<void> updateTransactions() async {
try {
if (_isTransactionUpdating) {
return;
}
_isTransactionUpdating = true;
final transactions = await fetchTransactions();
transactionHistory.addMany(transactions);
await transactionHistory.save();
_isTransactionUpdating = false;
} catch (e) {
print(e);
_isTransactionUpdating = false;
}
}
void addInitialAssets() {
final initialZanoAssets = DefaultZanoAssets().initialZanoAssets;
for (var token in initialZanoAssets) {
zanoAssetsBox.put(token.assetId, token);
}
}
2024-03-15 12:42:27 +00:00
Future<CryptoCurrency> addZanoAssetById(String assetId) async {
if (zanoAssetsBox.containsKey(assetId)) {
throw 'zano asset with id $assetId already added';
}
final assetDescriptor = await addAssetsWhitelist(assetId);
if (assetDescriptor == null) {
throw "there's no zano asset with id $assetId";
}
String? iconPath;
try {
iconPath = CryptoCurrency.all.firstWhere((element) => element.title.toUpperCase() == assetDescriptor.title.toUpperCase()).iconPath;
} catch (_) {}
final asset = ZanoAsset.copyWith(assetDescriptor, iconPath, 'ZANO', assetId: assetId, enabled: true);
await zanoAssetsBox.put(asset.assetId, ZanoAsset.copyWith(asset, iconPath, 'ZANO'));
balance[asset] = ZanoBalance(total: 0, unlocked: 0);
return asset;
}
2024-03-15 12:42:27 +00:00
Future<void> addRemoveZanoAsset(ZanoAsset asset) async {
String? iconPath;
try {
2024-03-15 12:42:27 +00:00
iconPath = CryptoCurrency.all.firstWhere((element) => element.title.toUpperCase() == asset.title.toUpperCase()).iconPath;
} catch (_) {}
2024-03-15 12:42:27 +00:00
await zanoAssetsBox.put(asset.assetId, ZanoAsset.copyWith(asset, iconPath, 'ZANO'));
if (asset.enabled) {
2024-03-15 12:42:27 +00:00
final assetDescriptor = await addAssetsWhitelist(asset.assetId);
if (assetDescriptor == null) {
print('error adding zano asset');
return;
}
balance[asset] = ZanoBalance(total: 0, unlocked: 0);
} else {
2024-03-15 12:42:27 +00:00
final result = await removeAssetsWhitelist(asset.assetId);
if (result == false) {
print('error removing zano asset');
return;
}
balance.removeWhere((key, _) => key is ZanoAsset && key.assetId == asset.assetId);
}
}
2024-03-15 12:42:27 +00:00
Future<void> deleteZanoAsset(ZanoAsset asset) async {
final result = await removeAssetsWhitelist(asset.assetId);
if (result == false) return;
await asset.delete();
balance.removeWhere((key, _) => key is ZanoAsset && key.assetId == asset.assetId);
}
Future<ZanoAsset?> getZanoAsset(String assetId) async {
2024-03-15 12:42:27 +00:00
return null;
}
2023-12-14 04:51:16 +00:00
// List<ZanoTransactionInfo> _getAllTransactions(dynamic _) =>
// zano_transaction_history
// .getAllTransations()
// .map((row) => ZanoTransactionInfo.fromRow(row))
// .toList();
2023-10-02 14:17:35 +00:00
// void _setListeners() {
// _listener?.stop();
// _listener = zano_wallet.setListeners(_onNewBlock, _onNewTransaction);
// }
2023-10-02 14:17:35 +00:00
Future<void> _askForUpdateTransactionHistory() async => await updateTransactions();
2023-10-02 14:17:35 +00:00
void _onNewBlock(int height, int blocksLeft, double ptc) async {
try {
if (walletInfo.isRecovery) {
await _askForUpdateTransactionHistory();
2023-11-17 17:40:23 +00:00
/*walletAddresses.accountList.update();*/
2023-10-02 14:17:35 +00:00
}
if (blocksLeft < 1000) {
await _askForUpdateTransactionHistory();
2023-11-17 17:40:23 +00:00
/*walletAddresses.accountList.update();*/
2023-10-02 14:17:35 +00:00
syncStatus = SyncedSyncStatus();
if (!_hasSyncAfterStartup) {
_hasSyncAfterStartup = true;
await save();
}
if (walletInfo.isRecovery) {
await setAsRecovered();
}
} else {
syncStatus = SyncingSyncStatus(blocksLeft, ptc);
}
} catch (e) {
print(e.toString());
}
}
void _onNewTransaction() async {
try {
await _askForUpdateTransactionHistory();
await Future<void>.delayed(Duration(seconds: 1)); // TODO: ???
2023-10-02 14:17:35 +00:00
} catch (e) {
print(e.toString());
}
}
2023-12-14 04:51:16 +00:00
Future<void> _refreshTransactions() async {
try {
2024-03-15 12:42:27 +00:00
history = await getRecentTxsAndInfo();
} catch (e) {
print(e.toString());
2024-03-06 06:48:59 +00:00
}
}
void _updateSyncProgress(GetWalletStatusResult walletStatus) {
final syncHeight = walletStatus.currentWalletHeight;
if (_initialSyncHeight <= 0) {
_initialSyncHeight = syncHeight;
}
final bchHeight = walletStatus.currentDaemonHeight;
if (_lastKnownBlockHeight == syncHeight) {
return;
}
_lastKnownBlockHeight = syncHeight;
final track = bchHeight - _initialSyncHeight;
final diff = track - (bchHeight - syncHeight);
final ptc = diff <= 0 ? 0.0 : diff / track;
final left = bchHeight - syncHeight;
if (syncHeight < 0 || left < 0) {
return;
}
// 1. Actual new height; 2. Blocks left to finish; 3. Progress in percents;
_onNewBlock.call(syncHeight, left, ptc);
}
2023-10-02 14:17:35 +00:00
}