cake_wallet/cw_zano/lib/zano_wallet.dart

525 lines
18 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';
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';
2024-03-16 10:55:03 +00:00
import 'package:cw_zano/api/model/transfer.dart';
2023-12-14 04:51:16 +00:00
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';
2024-03-16 10:55:03 +00:00
import 'package:cw_zano/zano_formatter.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;
2024-03-16 10:55:03 +00:00
List<Transfer> transfers = [];
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: '');
2024-03-16 10:55:03 +00:00
static const String zanoAssetId = 'd6329b5b1f7c0805b5c345f4957554002a2f557845f64d7645dae0e051a6498a';
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, decimalPoint: ZanoFormatter.defaultDecimalPoint)}),
_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 {
2024-03-16 10:55:03 +00:00
credentials as ZanoTransactionCredentials;
bool isZano() => credentials.currency == CryptoCurrency.zano;
final outputs = credentials.outputs;
2023-10-02 14:17:35 +00:00
final hasMultiDestination = outputs.length > 1;
2024-03-16 10:55:03 +00:00
final unlockedBalanceZano = BigInt.from(balance[CryptoCurrency.zano]?.unlocked ?? 0);
final unlockedBalanceCurrency = BigInt.from(balance[credentials.currency]?.unlocked ?? 0);
final fee = BigInt.from(calculateEstimatedFee(credentials.priority));
late BigInt totalAmount;
void checkForEnoughBalances() {
if (isZano()) {
if (totalAmount + fee > unlockedBalanceZano) {
throw ZanoTransactionCreationException(
"You don't have enough coins (required: ${ZanoFormatter.bigIntAmountToString(totalAmount + fee)} ZANO, unlocked ${ZanoFormatter.bigIntAmountToString(unlockedBalanceZano)} ZANO).");
}
} else {
if (fee > unlockedBalanceZano) {
throw ZanoTransactionCreationException(
"You don't have enough coins (required: ${ZanoFormatter.bigIntAmountToString(fee)} ZANO, unlocked ${ZanoFormatter.bigIntAmountToString(unlockedBalanceZano)} ZANO).");
}
if (totalAmount > unlockedBalanceCurrency) {
throw ZanoTransactionCreationException(
"You don't have enough coins (required: ${ZanoFormatter.bigIntAmountToString(totalAmount)} ${credentials.currency.title}, unlocked ${ZanoFormatter.bigIntAmountToString(unlockedBalanceZano)} ${credentials.currency.title}).");
}
}
}
final assetId = isZano() ? zanoAssetId : (currency as ZanoAsset).assetId;
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
}
2024-03-16 10:55:03 +00:00
totalAmount = outputs.fold(BigInt.zero, (acc, value) => acc + BigInt.from(value.formattedCryptoAmount ?? 0));
checkForEnoughBalances();
destinations = outputs
.map((output) => Destination(
2024-03-16 10:55:03 +00:00
amount: BigInt.from(output.formattedCryptoAmount ?? 0),
address: output.isParsedAddress ? output.extractedAddress! : output.address,
2024-03-16 10:55:03 +00:00
assetId: assetId,
))
2023-10-02 14:17:35 +00:00
.toList();
} else {
final output = outputs.first;
if (output.sendAll) {
2024-03-16 10:55:03 +00:00
if (isZano()) {
totalAmount = unlockedBalanceZano - fee;
} else {
totalAmount = unlockedBalanceCurrency;
}
} else {
2024-03-16 10:55:03 +00:00
totalAmount = BigInt.from(output.formattedCryptoAmount!);
2023-10-02 14:17:35 +00:00
}
2024-03-16 10:55:03 +00:00
checkForEnoughBalances();
destinations = [
Destination(
2024-03-16 10:55:03 +00:00
amount: totalAmount,
address: output.isParsedAddress ? output.extractedAddress! : output.address,
2024-03-16 10:55:03 +00:00
assetId: assetId,
)
];
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 ?? '',
2024-03-16 10:55:03 +00:00
assetId: assetId,
ticker: credentials.currency.title,
amount: totalAmount,
);
2023-10-02 14:17:35 +00:00
}
@override
Future<Map<String, ZanoTransactionInfo>> fetchTransactions() async {
2024-03-06 06:48:59 +00:00
try {
2024-03-16 10:55:03 +00:00
transfers = await getRecentTxsAndInfo();
return Map.fromIterable(
transfers,
key: (item) => (item as Transfer).txHash,
value: (item) {
item as Transfer;
if (item.subtransfers.first.assetId == zanoAssetId) {
return ZanoTransactionInfo.fromTransfer(item, 'ZANO', ZanoFormatter.defaultDecimalPoint);
2024-03-16 10:55:03 +00:00
} else {
final asset = zanoAssets.firstWhere((element) => element.assetId == item.subtransfers.first.assetId);
return ZanoTransactionInfo.fromTransfer(item, asset.ticker, asset.decimalPoint);
2024-03-16 10:55:03 +00:00
}
},
);
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 {
final boxName = '${walletInfo.name.replaceAll(' ', '_')}_${ZanoAsset.zanoAssetsBoxName}';
2024-03-15 12:42:27 +00:00
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, decimalPoint: asset.decimalPoint);
2024-03-15 12:42:27 +00:00
}
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-16 10:55:03 +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, decimalPoint: ZanoFormatter.defaultDecimalPoint);
2024-03-15 12:42:27 +00:00
} 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, decimalPoint: asset.decimalPoint);
2024-03-15 12:42:27 +00:00
}
}
}
}
//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, decimalPoint: asset.decimalPoint);
2024-03-15 12:42:27 +00:00
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, decimalPoint: asset.decimalPoint);
} 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
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
}