2024-01-08 19:40:07 +00:00
|
|
|
import 'dart:async';
|
2024-04-30 14:12:46 +00:00
|
|
|
import 'dart:io';
|
2024-01-17 19:10:23 +00:00
|
|
|
import 'dart:math';
|
2024-01-08 19:40:07 +00:00
|
|
|
|
|
|
|
import 'package:cw_core/monero_transaction_priority.dart';
|
|
|
|
import 'package:cw_core/node.dart';
|
|
|
|
import 'package:cw_core/pending_transaction.dart';
|
|
|
|
import 'package:cw_core/sync_status.dart';
|
|
|
|
import 'package:cw_core/transaction_direction.dart';
|
2024-09-30 15:36:12 +00:00
|
|
|
import 'package:cw_core/utxo.dart' as cw;
|
2024-01-08 19:40:07 +00:00
|
|
|
import 'package:cw_core/wallet_base.dart';
|
|
|
|
import 'package:cw_core/wallet_credentials.dart';
|
|
|
|
import 'package:cw_core/wallet_info.dart';
|
|
|
|
import 'package:cw_core/wallet_type.dart';
|
|
|
|
import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart';
|
|
|
|
import 'package:cw_monero/monero_wallet.dart';
|
2024-09-20 15:38:05 +00:00
|
|
|
import 'package:cw_monero/monero_wallet_service.dart';
|
2024-01-08 19:40:07 +00:00
|
|
|
import 'package:cw_monero/pending_monero_transaction.dart';
|
|
|
|
import 'package:decimal/decimal.dart';
|
|
|
|
import 'package:flutter_libmonero/core/wallet_creation_service.dart';
|
|
|
|
import 'package:flutter_libmonero/monero/monero.dart' as xmr_dart;
|
|
|
|
import 'package:flutter_libmonero/view_model/send/output.dart' as monero_output;
|
|
|
|
import 'package:isar/isar.dart';
|
2024-05-27 17:24:11 +00:00
|
|
|
import 'package:mutex/mutex.dart';
|
2024-01-08 19:40:07 +00:00
|
|
|
import 'package:tuple/tuple.dart';
|
|
|
|
|
2024-05-23 00:37:06 +00:00
|
|
|
import '../../../db/hive/db.dart';
|
|
|
|
import '../../../models/isar/models/blockchain_data/address.dart';
|
|
|
|
import '../../../models/isar/models/blockchain_data/transaction.dart';
|
2024-09-30 15:36:12 +00:00
|
|
|
import '../../../models/isar/models/blockchain_data/utxo.dart';
|
2024-07-04 21:17:05 +00:00
|
|
|
import '../../../models/keys/cw_key_data.dart';
|
2024-05-27 17:24:11 +00:00
|
|
|
import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart';
|
|
|
|
import '../../../services/event_bus/events/global/tor_status_changed_event.dart';
|
|
|
|
import '../../../services/event_bus/global_event_bus.dart';
|
|
|
|
import '../../../services/tor_service.dart';
|
2024-05-23 00:37:06 +00:00
|
|
|
import '../../../utilities/amount/amount.dart';
|
|
|
|
import '../../../utilities/enums/fee_rate_type_enum.dart';
|
|
|
|
import '../../../utilities/logger.dart';
|
|
|
|
import '../../crypto_currency/crypto_currency.dart';
|
|
|
|
import '../../models/tx_data.dart';
|
|
|
|
import '../intermediate/cryptonote_wallet.dart';
|
|
|
|
import '../wallet.dart';
|
|
|
|
import '../wallet_mixin_interfaces/cw_based_interface.dart';
|
2024-01-08 19:40:07 +00:00
|
|
|
|
2024-01-12 17:59:07 +00:00
|
|
|
class MoneroWallet extends CryptonoteWallet with CwBasedInterface {
|
2024-05-04 00:56:27 +00:00
|
|
|
MoneroWallet(CryptoCurrencyNetwork network) : super(Monero(network)) {
|
|
|
|
final bus = GlobalEventBus.instance;
|
|
|
|
|
|
|
|
// Listen for tor status changes.
|
|
|
|
_torStatusListener = bus.on<TorConnectionStatusChangedEvent>().listen(
|
|
|
|
(event) async {
|
|
|
|
switch (event.newStatus) {
|
|
|
|
case TorConnectionStatus.connecting:
|
|
|
|
if (!_torConnectingLock.isLocked) {
|
|
|
|
await _torConnectingLock.acquire();
|
|
|
|
}
|
|
|
|
_requireMutex = true;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case TorConnectionStatus.connected:
|
|
|
|
case TorConnectionStatus.disconnected:
|
|
|
|
if (_torConnectingLock.isLocked) {
|
|
|
|
_torConnectingLock.release();
|
|
|
|
}
|
|
|
|
_requireMutex = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
// Listen for tor preference changes.
|
|
|
|
_torPreferenceListener = bus.on<TorPreferenceChangedEvent>().listen(
|
|
|
|
(event) async {
|
|
|
|
await updateNode();
|
|
|
|
},
|
|
|
|
);
|
2024-09-30 15:36:12 +00:00
|
|
|
|
|
|
|
// Potentially dangerous hack. See comments in _startInit()
|
|
|
|
_startInit();
|
|
|
|
}
|
|
|
|
|
|
|
|
// cw based wallet listener to handle synchronization of utxo frozen states
|
|
|
|
late final StreamSubscription<List<UTXO>> _streamSub;
|
|
|
|
Future<void> _startInit() async {
|
|
|
|
// Delay required as `mainDB` is not initialized in constructor.
|
|
|
|
// This is a hack and could lead to a race condition.
|
|
|
|
Future.delayed(const Duration(seconds: 2), () {
|
|
|
|
_streamSub = mainDB.isar.utxos
|
|
|
|
.where()
|
|
|
|
.walletIdEqualTo(walletId)
|
|
|
|
.watch(fireImmediately: true)
|
|
|
|
.listen((utxos) async {
|
|
|
|
await onUTXOsCHanged(utxos);
|
|
|
|
await updateBalance(shouldUpdateUtxos: false);
|
|
|
|
});
|
|
|
|
});
|
2024-05-04 00:56:27 +00:00
|
|
|
}
|
2024-01-08 19:40:07 +00:00
|
|
|
|
2024-09-20 15:38:05 +00:00
|
|
|
@override
|
|
|
|
MoneroWalletBase? cwWalletBase;
|
|
|
|
|
|
|
|
@override
|
|
|
|
MoneroWalletService? cwWalletService;
|
|
|
|
|
2024-01-08 19:40:07 +00:00
|
|
|
@override
|
2024-01-12 17:59:07 +00:00
|
|
|
Address addressFor({required int index, int account = 0}) {
|
2024-09-20 15:38:05 +00:00
|
|
|
final String address = (cwWalletBase as MoneroWalletBase)
|
2024-01-12 17:59:07 +00:00
|
|
|
.getTransactionAddress(account, index);
|
|
|
|
|
|
|
|
final newReceivingAddress = Address(
|
|
|
|
walletId: walletId,
|
|
|
|
derivationIndex: index,
|
|
|
|
derivationPath: null,
|
|
|
|
value: address,
|
|
|
|
publicKey: [],
|
|
|
|
type: AddressType.cryptonote,
|
|
|
|
subType: AddressSubType.receiving,
|
|
|
|
);
|
|
|
|
|
|
|
|
return newReceivingAddress;
|
|
|
|
}
|
2024-01-08 19:40:07 +00:00
|
|
|
|
2024-01-12 17:59:07 +00:00
|
|
|
@override
|
|
|
|
Future<void> open() async {
|
|
|
|
String? password;
|
|
|
|
try {
|
|
|
|
password = await cwKeysStorage.getWalletPassword(walletName: walletId);
|
|
|
|
} catch (e, s) {
|
|
|
|
throw Exception("Password not found $e, $s");
|
|
|
|
}
|
2024-01-08 19:40:07 +00:00
|
|
|
|
2024-09-20 15:38:05 +00:00
|
|
|
bool wasNull = false;
|
2024-01-08 19:40:07 +00:00
|
|
|
|
2024-09-20 15:38:05 +00:00
|
|
|
if (cwWalletBase == null) {
|
|
|
|
wasNull = true;
|
|
|
|
// cwWalletBaseT?.close();
|
|
|
|
cwWalletBase ??= (await cwWalletService!.openWallet(walletId, password))
|
|
|
|
as MoneroWalletBase;
|
2024-01-08 19:40:07 +00:00
|
|
|
|
2024-09-20 15:38:05 +00:00
|
|
|
cwWalletBase?.onNewBlock ??= onNewBlock;
|
|
|
|
cwWalletBase?.onNewTransaction ??= onNewTransaction;
|
|
|
|
cwWalletBase?.syncStatusChanged ??= syncStatusChanged;
|
|
|
|
|
|
|
|
await updateNode();
|
|
|
|
}
|
2024-01-08 19:40:07 +00:00
|
|
|
|
2024-06-04 15:40:45 +00:00
|
|
|
Address? currentAddress = await getCurrentReceivingAddress();
|
|
|
|
if (currentAddress == null) {
|
|
|
|
currentAddress = addressFor(index: 0);
|
|
|
|
await mainDB.updateOrPutAddresses([currentAddress]);
|
|
|
|
}
|
|
|
|
if (info.cachedReceivingAddress != currentAddress.value) {
|
|
|
|
await info.updateReceivingAddress(
|
|
|
|
newAddress: currentAddress.value,
|
|
|
|
isar: mainDB.isar,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-09-20 15:38:05 +00:00
|
|
|
if (wasNull) {
|
|
|
|
await cwWalletBase?.startSync();
|
|
|
|
} else {
|
|
|
|
cwWalletBase?.wallet.startListeners();
|
|
|
|
}
|
2024-01-12 17:59:07 +00:00
|
|
|
unawaited(refresh());
|
2024-09-20 15:38:05 +00:00
|
|
|
|
2024-01-12 17:59:07 +00:00
|
|
|
autoSaveTimer?.cancel();
|
|
|
|
autoSaveTimer = Timer.periodic(
|
|
|
|
const Duration(seconds: 193),
|
2024-09-20 15:38:05 +00:00
|
|
|
(_) async => await cwWalletBase?.save(),
|
2024-01-12 17:59:07 +00:00
|
|
|
);
|
|
|
|
}
|
2024-01-08 19:40:07 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
Future<Amount> estimateFeeFor(Amount amount, int feeRate) async {
|
2024-09-20 15:38:05 +00:00
|
|
|
if (cwWalletBase == null || cwWalletBase?.syncStatus is! SyncedSyncStatus) {
|
2024-01-12 17:59:07 +00:00
|
|
|
return Amount.zeroWith(
|
|
|
|
fractionDigits: cryptoCurrency.fractionDigits,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-01-08 19:40:07 +00:00
|
|
|
MoneroTransactionPriority priority;
|
|
|
|
switch (feeRate) {
|
|
|
|
case 1:
|
|
|
|
priority = MoneroTransactionPriority.regular;
|
|
|
|
break;
|
|
|
|
case 2:
|
|
|
|
priority = MoneroTransactionPriority.medium;
|
|
|
|
break;
|
|
|
|
case 3:
|
|
|
|
priority = MoneroTransactionPriority.fast;
|
|
|
|
break;
|
|
|
|
case 4:
|
|
|
|
priority = MoneroTransactionPriority.fastest;
|
|
|
|
break;
|
|
|
|
case 0:
|
|
|
|
default:
|
|
|
|
priority = MoneroTransactionPriority.slow;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2024-01-12 17:59:07 +00:00
|
|
|
int approximateFee = 0;
|
2024-01-08 19:40:07 +00:00
|
|
|
await estimateFeeMutex.protect(() async {
|
2024-09-20 15:38:05 +00:00
|
|
|
approximateFee = cwWalletBase!.calculateEstimatedFee(
|
2024-01-12 17:59:07 +00:00
|
|
|
priority,
|
|
|
|
amount.raw.toInt(),
|
2024-01-08 19:40:07 +00:00
|
|
|
);
|
2024-01-12 17:59:07 +00:00
|
|
|
});
|
2024-01-08 19:40:07 +00:00
|
|
|
|
2024-01-12 17:59:07 +00:00
|
|
|
return Amount(
|
|
|
|
rawValue: BigInt.from(approximateFee),
|
|
|
|
fractionDigits: cryptoCurrency.fractionDigits,
|
2024-01-08 19:40:07 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2024-01-12 17:59:07 +00:00
|
|
|
Future<bool> pingCheck() async {
|
2024-09-20 15:38:05 +00:00
|
|
|
return await cwWalletBase?.isConnected() ?? false;
|
2024-01-08 19:40:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> updateNode() async {
|
|
|
|
final node = getCurrentNode();
|
|
|
|
|
2024-07-30 03:54:23 +00:00
|
|
|
String host = Uri.parse(node.host).host;
|
|
|
|
if (host.isEmpty) {
|
|
|
|
host = node.host;
|
|
|
|
}
|
2024-04-30 14:12:46 +00:00
|
|
|
({InternetAddress host, int port})? proxy;
|
|
|
|
if (prefs.useTor) {
|
|
|
|
proxy = TorService.sharedInstance.getProxyInfo();
|
|
|
|
}
|
2024-05-04 00:56:27 +00:00
|
|
|
if (_requireMutex) {
|
|
|
|
await _torConnectingLock.protect(() async {
|
2024-09-20 15:38:05 +00:00
|
|
|
await cwWalletBase?.connectToNode(
|
2024-05-04 00:56:27 +00:00
|
|
|
node: Node(
|
|
|
|
uri: "$host:${node.port}",
|
|
|
|
type: WalletType.monero,
|
|
|
|
trusted: node.trusted ?? false,
|
2024-06-04 15:16:57 +00:00
|
|
|
useSSL: node.useSSL,
|
2024-05-04 00:56:27 +00:00
|
|
|
),
|
|
|
|
socksProxyAddress:
|
|
|
|
proxy == null ? null : "${proxy.host.address}:${proxy.port}",
|
|
|
|
);
|
|
|
|
});
|
|
|
|
} else {
|
2024-09-20 15:38:05 +00:00
|
|
|
await cwWalletBase?.connectToNode(
|
2024-05-04 00:56:27 +00:00
|
|
|
node: Node(
|
|
|
|
uri: "$host:${node.port}",
|
|
|
|
type: WalletType.monero,
|
|
|
|
trusted: node.trusted ?? false,
|
2024-06-04 15:16:57 +00:00
|
|
|
useSSL: node.useSSL,
|
2024-05-04 00:56:27 +00:00
|
|
|
),
|
|
|
|
socksProxyAddress:
|
|
|
|
proxy == null ? null : "${proxy.host.address}:${proxy.port}",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
2024-01-08 19:40:07 +00:00
|
|
|
}
|
|
|
|
|
2024-07-04 21:17:05 +00:00
|
|
|
@override
|
|
|
|
Future<CWKeyData?> getKeys() async {
|
2024-09-20 15:38:05 +00:00
|
|
|
final base = cwWalletBase;
|
2024-07-04 21:17:05 +00:00
|
|
|
|
2024-09-20 15:38:05 +00:00
|
|
|
if (base == null || base.walletInfo.name != walletId) {
|
2024-07-04 21:17:05 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return CWKeyData(
|
|
|
|
walletId: walletId,
|
|
|
|
publicViewKey: base.keys.publicViewKey,
|
|
|
|
privateViewKey: base.keys.privateViewKey,
|
|
|
|
publicSpendKey: base.keys.publicSpendKey,
|
|
|
|
privateSpendKey: base.keys.privateSpendKey,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-01-08 19:40:07 +00:00
|
|
|
@override
|
|
|
|
Future<void> updateTransactions() async {
|
2024-09-20 15:38:05 +00:00
|
|
|
final base = cwWalletBase;
|
2024-01-23 03:24:30 +00:00
|
|
|
|
2024-09-20 15:38:05 +00:00
|
|
|
if (base == null || base.walletInfo.name != walletId) {
|
2024-01-27 22:53:37 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
await base.updateTransactions();
|
|
|
|
final transactions = base.transactionHistory?.transactions;
|
2024-01-08 19:40:07 +00:00
|
|
|
|
|
|
|
// final cachedTransactions =
|
|
|
|
// DB.instance.get<dynamic>(boxName: walletId, key: 'latest_tx_model')
|
|
|
|
// as TransactionData?;
|
|
|
|
// int latestTxnBlockHeight =
|
|
|
|
// DB.instance.get<dynamic>(boxName: walletId, key: "storedTxnDataHeight")
|
|
|
|
// as int? ??
|
|
|
|
// 0;
|
|
|
|
//
|
|
|
|
// final txidsList = DB.instance
|
|
|
|
// .get<dynamic>(boxName: walletId, key: "cachedTxids") as List? ??
|
|
|
|
// [];
|
|
|
|
//
|
|
|
|
// final Set<String> cachedTxids = Set<String>.from(txidsList);
|
|
|
|
|
|
|
|
// TODO: filter to skip cached + confirmed txn processing in next step
|
|
|
|
// final unconfirmedCachedTransactions =
|
|
|
|
// cachedTransactions?.getAllTransactions() ?? {};
|
|
|
|
// unconfirmedCachedTransactions
|
|
|
|
// .removeWhere((key, value) => value.confirmedStatus);
|
|
|
|
//
|
|
|
|
// if (cachedTransactions != null) {
|
|
|
|
// for (final tx in allTxHashes.toList(growable: false)) {
|
|
|
|
// final txHeight = tx["height"] as int;
|
|
|
|
// if (txHeight > 0 &&
|
|
|
|
// txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) {
|
|
|
|
// if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) {
|
|
|
|
// allTxHashes.remove(tx);
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
|
|
final List<Tuple2<Transaction, Address?>> txnsData = [];
|
|
|
|
|
|
|
|
if (transactions != null) {
|
2024-01-12 17:59:07 +00:00
|
|
|
for (final tx in transactions.entries) {
|
2024-01-08 19:40:07 +00:00
|
|
|
Address? address;
|
|
|
|
TransactionType type;
|
|
|
|
if (tx.value.direction == TransactionDirection.incoming) {
|
|
|
|
final addressInfo = tx.value.additionalInfo;
|
|
|
|
|
2024-09-20 15:38:05 +00:00
|
|
|
final addressString = cwWalletBase?.getTransactionAddress(
|
2024-01-08 19:40:07 +00:00
|
|
|
addressInfo!['accountIndex'] as int,
|
|
|
|
addressInfo['addressIndex'] as int,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (addressString != null) {
|
|
|
|
address = await mainDB
|
|
|
|
.getAddresses(walletId)
|
|
|
|
.filter()
|
|
|
|
.valueEqualTo(addressString)
|
|
|
|
.findFirst();
|
|
|
|
}
|
|
|
|
|
|
|
|
type = TransactionType.incoming;
|
|
|
|
} else {
|
|
|
|
// txn.address = "";
|
|
|
|
type = TransactionType.outgoing;
|
|
|
|
}
|
|
|
|
|
|
|
|
final txn = Transaction(
|
|
|
|
walletId: walletId,
|
|
|
|
txid: tx.value.id,
|
|
|
|
timestamp: (tx.value.date.millisecondsSinceEpoch ~/ 1000),
|
|
|
|
type: type,
|
|
|
|
subType: TransactionSubType.none,
|
|
|
|
amount: tx.value.amount ?? 0,
|
|
|
|
amountString: Amount(
|
|
|
|
rawValue: BigInt.from(tx.value.amount ?? 0),
|
|
|
|
fractionDigits: cryptoCurrency.fractionDigits,
|
|
|
|
).toJsonString(),
|
|
|
|
fee: tx.value.fee ?? 0,
|
|
|
|
height: tx.value.height,
|
|
|
|
isCancelled: false,
|
|
|
|
isLelantus: false,
|
|
|
|
slateId: null,
|
|
|
|
otherData: null,
|
|
|
|
nonce: null,
|
|
|
|
inputs: [],
|
|
|
|
outputs: [],
|
|
|
|
numberOfMessages: null,
|
|
|
|
);
|
|
|
|
|
|
|
|
txnsData.add(Tuple2(txn, address));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-27 22:53:37 +00:00
|
|
|
await mainDB.isar.writeTxn(() async {
|
|
|
|
await mainDB.isar.transactions
|
|
|
|
.where()
|
|
|
|
.walletIdEqualTo(walletId)
|
|
|
|
.deleteAll();
|
|
|
|
for (final data in txnsData) {
|
|
|
|
final tx = data.item1;
|
|
|
|
|
|
|
|
// save transaction
|
|
|
|
await mainDB.isar.transactions.put(tx);
|
|
|
|
|
|
|
|
if (data.item2 != null) {
|
|
|
|
final address = await mainDB.getAddress(walletId, data.item2!.value);
|
|
|
|
|
|
|
|
// check if address exists in db and add if it does not
|
|
|
|
if (address == null) {
|
|
|
|
await mainDB.isar.addresses.put(data.item2!);
|
|
|
|
}
|
|
|
|
|
|
|
|
// link and save address
|
|
|
|
tx.address.value = address ?? data.item2!;
|
|
|
|
await tx.address.save();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2024-01-08 19:40:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2024-01-17 18:52:33 +00:00
|
|
|
Future<void> init({bool? isRestore}) async {
|
2024-09-20 15:38:05 +00:00
|
|
|
cwWalletService ??= xmr_dart.monero
|
|
|
|
.createMoneroWalletService(DB.instance.moneroWalletInfoBox)
|
|
|
|
as MoneroWalletService;
|
2024-01-08 19:40:07 +00:00
|
|
|
|
2024-09-20 15:38:05 +00:00
|
|
|
if (!(await cwWalletService!.isWalletExist(walletId)) &&
|
2024-01-27 22:53:37 +00:00
|
|
|
isRestore != true) {
|
2024-01-08 19:40:07 +00:00
|
|
|
WalletInfo walletInfo;
|
|
|
|
WalletCredentials credentials;
|
|
|
|
try {
|
|
|
|
final dirPath =
|
2024-01-17 18:52:33 +00:00
|
|
|
await pathForWalletDir(name: walletId, type: WalletType.monero);
|
|
|
|
final path =
|
|
|
|
await pathForWallet(name: walletId, type: WalletType.monero);
|
2024-01-08 19:40:07 +00:00
|
|
|
credentials = xmr_dart.monero.createMoneroNewWalletCredentials(
|
2024-01-17 18:52:33 +00:00
|
|
|
name: walletId,
|
2024-01-08 19:40:07 +00:00
|
|
|
language: "English",
|
|
|
|
);
|
|
|
|
|
|
|
|
walletInfo = WalletInfo.external(
|
2024-01-17 18:52:33 +00:00
|
|
|
id: WalletBase.idFor(walletId, WalletType.monero),
|
|
|
|
name: walletId,
|
2024-01-08 19:40:07 +00:00
|
|
|
type: WalletType.monero,
|
|
|
|
isRecovery: false,
|
|
|
|
restoreHeight: credentials.height ?? 0,
|
|
|
|
date: DateTime.now(),
|
|
|
|
path: path,
|
|
|
|
dirPath: dirPath,
|
|
|
|
address: '',
|
|
|
|
);
|
|
|
|
credentials.walletInfo = walletInfo;
|
|
|
|
|
|
|
|
final _walletCreationService = WalletCreationService(
|
|
|
|
secureStorage: secureStorageInterface,
|
2024-09-20 15:38:05 +00:00
|
|
|
walletService: cwWalletService!,
|
2024-01-08 19:40:07 +00:00
|
|
|
keyService: cwKeysStorage,
|
2024-09-20 15:38:05 +00:00
|
|
|
type: WalletType.monero,
|
2024-01-08 19:40:07 +00:00
|
|
|
);
|
|
|
|
// To restore from a seed
|
|
|
|
final wallet = await _walletCreationService.create(credentials);
|
|
|
|
|
|
|
|
// subtract a couple days to ensure we have a buffer for SWB
|
|
|
|
final bufferedCreateHeight = xmr_dart.monero.getHeigthByDate(
|
2024-05-27 17:24:11 +00:00
|
|
|
date: DateTime.now().subtract(const Duration(days: 2)),
|
|
|
|
);
|
2024-01-08 19:40:07 +00:00
|
|
|
|
|
|
|
await info.updateRestoreHeight(
|
|
|
|
newRestoreHeight: bufferedCreateHeight,
|
|
|
|
isar: mainDB.isar,
|
|
|
|
);
|
|
|
|
|
|
|
|
// special case for xmr/wow. Normally mnemonic + passphrase is saved
|
|
|
|
// before wallet.init() is called
|
|
|
|
await secureStorageInterface.write(
|
|
|
|
key: Wallet.mnemonicKey(walletId: walletId),
|
|
|
|
value: wallet.seed.trim(),
|
|
|
|
);
|
|
|
|
await secureStorageInterface.write(
|
|
|
|
key: Wallet.mnemonicPassphraseKey(walletId: walletId),
|
|
|
|
value: "",
|
|
|
|
);
|
|
|
|
|
|
|
|
walletInfo.restoreHeight = bufferedCreateHeight;
|
|
|
|
|
|
|
|
walletInfo.address = wallet.walletAddresses.address;
|
|
|
|
await DB.instance
|
|
|
|
.add<WalletInfo>(boxName: WalletInfo.boxName, value: walletInfo);
|
|
|
|
|
2024-01-12 17:59:07 +00:00
|
|
|
wallet.close();
|
2024-01-08 19:40:07 +00:00
|
|
|
} catch (e, s) {
|
|
|
|
Logging.instance.log("$e\n$s", level: LogLevel.Fatal);
|
2024-09-20 15:38:05 +00:00
|
|
|
cwWalletBase?.close();
|
2024-01-08 19:40:07 +00:00
|
|
|
}
|
|
|
|
await updateNode();
|
|
|
|
}
|
|
|
|
|
|
|
|
return super.init();
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> recover({required bool isRescan}) async {
|
|
|
|
if (isRescan) {
|
|
|
|
await refreshMutex.protect(() async {
|
|
|
|
// clear blockchain info
|
|
|
|
await mainDB.deleteWalletBlockchainData(walletId);
|
|
|
|
|
2024-09-20 15:38:05 +00:00
|
|
|
final restoreHeight = cwWalletBase?.walletInfo.restoreHeight;
|
2024-01-12 17:59:07 +00:00
|
|
|
highestPercentCached = 0;
|
2024-09-20 15:38:05 +00:00
|
|
|
await cwWalletBase?.rescan(height: restoreHeight ?? 0);
|
2024-01-08 19:40:07 +00:00
|
|
|
});
|
2024-01-12 17:59:07 +00:00
|
|
|
unawaited(refresh());
|
2024-01-08 19:40:07 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
await refreshMutex.protect(() async {
|
|
|
|
final mnemonic = await getMnemonic();
|
|
|
|
final seedLength = mnemonic.trim().split(" ").length;
|
|
|
|
|
2024-04-30 07:55:24 +00:00
|
|
|
if (seedLength != 25 && seedLength != 16) {
|
2024-01-08 19:40:07 +00:00
|
|
|
throw Exception("Invalid monero mnemonic length found: $seedLength");
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2024-01-17 19:10:23 +00:00
|
|
|
final height = max(info.restoreHeight, 0);
|
2024-01-08 19:40:07 +00:00
|
|
|
|
2024-01-12 17:59:07 +00:00
|
|
|
await info.updateRestoreHeight(
|
|
|
|
newRestoreHeight: height,
|
|
|
|
isar: mainDB.isar,
|
|
|
|
);
|
2024-01-08 19:40:07 +00:00
|
|
|
|
2024-09-20 15:38:05 +00:00
|
|
|
cwWalletService = xmr_dart.monero
|
|
|
|
.createMoneroWalletService(DB.instance.moneroWalletInfoBox)
|
|
|
|
as MoneroWalletService;
|
2024-01-08 19:40:07 +00:00
|
|
|
WalletInfo walletInfo;
|
|
|
|
WalletCredentials credentials;
|
2024-05-04 00:56:39 +00:00
|
|
|
final String name = walletId;
|
2024-01-08 19:40:07 +00:00
|
|
|
final dirPath =
|
2024-01-12 17:59:07 +00:00
|
|
|
await pathForWalletDir(name: name, type: WalletType.monero);
|
|
|
|
final path = await pathForWallet(name: name, type: WalletType.monero);
|
2024-01-08 19:40:07 +00:00
|
|
|
credentials =
|
|
|
|
xmr_dart.monero.createMoneroRestoreWalletFromSeedCredentials(
|
|
|
|
name: name,
|
|
|
|
height: height,
|
|
|
|
mnemonic: mnemonic.trim(),
|
|
|
|
);
|
|
|
|
try {
|
|
|
|
walletInfo = WalletInfo.external(
|
2024-01-12 17:59:07 +00:00
|
|
|
id: WalletBase.idFor(name, WalletType.monero),
|
|
|
|
name: name,
|
|
|
|
type: WalletType.monero,
|
|
|
|
isRecovery: false,
|
|
|
|
restoreHeight: credentials.height ?? 0,
|
|
|
|
date: DateTime.now(),
|
|
|
|
path: path,
|
|
|
|
dirPath: dirPath,
|
|
|
|
address: '',
|
|
|
|
);
|
2024-01-08 19:40:07 +00:00
|
|
|
credentials.walletInfo = walletInfo;
|
|
|
|
|
2024-01-12 17:59:07 +00:00
|
|
|
final cwWalletCreationService = WalletCreationService(
|
2024-01-08 19:40:07 +00:00
|
|
|
secureStorage: secureStorageInterface,
|
2024-09-20 15:38:05 +00:00
|
|
|
walletService: cwWalletService!,
|
2024-01-08 19:40:07 +00:00
|
|
|
keyService: cwKeysStorage,
|
2024-09-20 15:38:05 +00:00
|
|
|
type: WalletType.monero,
|
2024-01-08 19:40:07 +00:00
|
|
|
);
|
|
|
|
// To restore from a seed
|
|
|
|
final wallet =
|
2024-01-12 17:59:07 +00:00
|
|
|
await cwWalletCreationService.restoreFromSeed(credentials);
|
2024-01-08 19:40:07 +00:00
|
|
|
walletInfo.address = wallet.walletAddresses.address;
|
|
|
|
await DB.instance
|
|
|
|
.add<WalletInfo>(boxName: WalletInfo.boxName, value: walletInfo);
|
2024-09-27 20:31:05 +00:00
|
|
|
cwWalletBase?.close();
|
|
|
|
cwWalletBase = wallet as MoneroWalletBase;
|
|
|
|
cwWalletBase?.onNewBlock = onNewBlock;
|
|
|
|
cwWalletBase?.onNewTransaction = onNewTransaction;
|
|
|
|
cwWalletBase?.syncStatusChanged = syncStatusChanged;
|
2024-01-18 19:00:50 +00:00
|
|
|
if (walletInfo.address != null) {
|
|
|
|
final newReceivingAddress = await getCurrentReceivingAddress() ??
|
|
|
|
Address(
|
|
|
|
walletId: walletId,
|
|
|
|
derivationIndex: 0,
|
|
|
|
derivationPath: null,
|
|
|
|
value: walletInfo.address!,
|
|
|
|
publicKey: [],
|
|
|
|
type: AddressType.cryptonote,
|
|
|
|
subType: AddressSubType.receiving,
|
|
|
|
);
|
|
|
|
|
|
|
|
await mainDB.updateOrPutAddresses([newReceivingAddress]);
|
|
|
|
await info.updateReceivingAddress(
|
|
|
|
newAddress: newReceivingAddress.value,
|
|
|
|
isar: mainDB.isar,
|
|
|
|
);
|
|
|
|
}
|
2024-01-08 19:40:07 +00:00
|
|
|
} catch (e, s) {
|
|
|
|
Logging.instance.log("$e\n$s", level: LogLevel.Fatal);
|
|
|
|
}
|
|
|
|
await updateNode();
|
|
|
|
|
2024-09-20 15:38:05 +00:00
|
|
|
await cwWalletBase?.rescan(height: credentials.height);
|
2024-01-08 19:40:07 +00:00
|
|
|
} catch (e, s) {
|
|
|
|
Logging.instance.log(
|
2024-05-27 17:24:11 +00:00
|
|
|
"Exception rethrown from recoverFromMnemonic(): $e\n$s",
|
|
|
|
level: LogLevel.Error,
|
|
|
|
);
|
2024-01-08 19:40:07 +00:00
|
|
|
rethrow;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<TxData> prepareSend({required TxData txData}) async {
|
|
|
|
try {
|
|
|
|
final feeRate = txData.feeRateType;
|
|
|
|
if (feeRate is FeeRateType) {
|
|
|
|
MoneroTransactionPriority feePriority;
|
|
|
|
switch (feeRate) {
|
|
|
|
case FeeRateType.fast:
|
|
|
|
feePriority = MoneroTransactionPriority.fast;
|
|
|
|
break;
|
|
|
|
case FeeRateType.average:
|
|
|
|
feePriority = MoneroTransactionPriority.regular;
|
|
|
|
break;
|
|
|
|
case FeeRateType.slow:
|
|
|
|
feePriority = MoneroTransactionPriority.slow;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw ArgumentError("Invalid use of custom fee");
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<PendingTransaction>? awaitPendingTransaction;
|
|
|
|
try {
|
|
|
|
// check for send all
|
|
|
|
bool isSendAll = false;
|
2024-01-12 17:59:07 +00:00
|
|
|
final balance = await availableBalance;
|
2024-01-08 19:40:07 +00:00
|
|
|
if (txData.amount! == balance &&
|
|
|
|
txData.recipients!.first.amount == balance) {
|
|
|
|
isSendAll = true;
|
|
|
|
}
|
|
|
|
|
2024-05-04 00:56:39 +00:00
|
|
|
final List<monero_output.Output> outputs = [];
|
2024-01-08 19:40:07 +00:00
|
|
|
for (final recipient in txData.recipients!) {
|
2024-09-20 15:38:05 +00:00
|
|
|
final output = monero_output.Output(cwWalletBase!);
|
2024-01-08 19:40:07 +00:00
|
|
|
output.address = recipient.address;
|
|
|
|
output.sendAll = isSendAll;
|
2024-05-04 00:56:39 +00:00
|
|
|
final String amountToSend = recipient.amount.decimal.toString();
|
2024-01-08 19:40:07 +00:00
|
|
|
output.setCryptoAmount(amountToSend);
|
2024-01-16 19:29:44 +00:00
|
|
|
outputs.add(output);
|
2024-01-08 19:40:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
final tmp =
|
|
|
|
xmr_dart.monero.createMoneroTransactionCreationCredentials(
|
|
|
|
outputs: outputs,
|
|
|
|
priority: feePriority,
|
|
|
|
);
|
|
|
|
|
2024-09-30 15:36:12 +00:00
|
|
|
final height = await chainHeight;
|
|
|
|
final inputs = txData.utxos
|
|
|
|
?.map(
|
|
|
|
(e) => cw.UTXO(
|
|
|
|
address: e.address!,
|
|
|
|
hash: e.txid,
|
|
|
|
keyImage: e.keyImage!,
|
|
|
|
value: e.value,
|
|
|
|
isFrozen: e.isBlocked,
|
|
|
|
isUnlocked: e.blockHeight != null &&
|
|
|
|
(height - (e.blockHeight ?? 0)) >=
|
|
|
|
cryptoCurrency.minConfirms,
|
|
|
|
height: e.blockHeight ?? 0,
|
|
|
|
vout: e.vout,
|
|
|
|
spent: e.used ?? false,
|
|
|
|
coinbase: e.isCoinbase,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
.toList();
|
|
|
|
|
2024-01-08 19:40:07 +00:00
|
|
|
await prepareSendMutex.protect(() async {
|
2024-09-30 15:36:12 +00:00
|
|
|
awaitPendingTransaction = cwWalletBase!.createTransaction(
|
|
|
|
tmp,
|
|
|
|
inputs: inputs,
|
|
|
|
);
|
2024-01-08 19:40:07 +00:00
|
|
|
});
|
|
|
|
} catch (e, s) {
|
2024-05-27 17:24:11 +00:00
|
|
|
Logging.instance.log(
|
|
|
|
"Exception rethrown from prepareSend(): $e\n$s",
|
|
|
|
level: LogLevel.Warning,
|
|
|
|
);
|
2024-01-08 19:40:07 +00:00
|
|
|
}
|
|
|
|
|
2024-05-04 00:56:39 +00:00
|
|
|
final PendingMoneroTransaction pendingMoneroTransaction =
|
2024-01-08 19:40:07 +00:00
|
|
|
await (awaitPendingTransaction!) as PendingMoneroTransaction;
|
|
|
|
final realFee = Amount.fromDecimal(
|
|
|
|
Decimal.parse(pendingMoneroTransaction.feeFormatted),
|
|
|
|
fractionDigits: cryptoCurrency.fractionDigits,
|
|
|
|
);
|
|
|
|
|
|
|
|
return txData.copyWith(
|
|
|
|
fee: realFee,
|
|
|
|
pendingMoneroTransaction: pendingMoneroTransaction,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
throw ArgumentError("Invalid fee rate argument provided!");
|
|
|
|
}
|
|
|
|
} catch (e, s) {
|
2024-05-27 17:24:11 +00:00
|
|
|
Logging.instance.log(
|
|
|
|
"Exception rethrown from prepare send(): $e\n$s",
|
|
|
|
level: LogLevel.Info,
|
|
|
|
);
|
2024-01-08 19:40:07 +00:00
|
|
|
|
|
|
|
if (e.toString().contains("Incorrect unlocked balance")) {
|
|
|
|
throw Exception("Insufficient balance!");
|
|
|
|
} else if (e is CreationTransactionException) {
|
|
|
|
throw Exception("Insufficient funds to pay for transaction fee!");
|
|
|
|
} else {
|
|
|
|
throw Exception("Transaction failed with error code $e");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<TxData> confirmSend({required TxData txData}) async {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
await txData.pendingMoneroTransaction!.commit();
|
|
|
|
Logging.instance.log(
|
2024-05-27 17:24:11 +00:00
|
|
|
"transaction ${txData.pendingMoneroTransaction!.id} has been sent",
|
|
|
|
level: LogLevel.Info,
|
|
|
|
);
|
2024-01-08 19:40:07 +00:00
|
|
|
return txData.copyWith(txid: txData.pendingMoneroTransaction!.id);
|
|
|
|
} catch (e, s) {
|
2024-05-27 17:24:11 +00:00
|
|
|
Logging.instance.log(
|
|
|
|
"${info.name} monero confirmSend: $e\n$s",
|
|
|
|
level: LogLevel.Error,
|
|
|
|
);
|
2024-01-08 19:40:07 +00:00
|
|
|
rethrow;
|
|
|
|
}
|
|
|
|
} catch (e, s) {
|
2024-05-27 17:24:11 +00:00
|
|
|
Logging.instance.log(
|
|
|
|
"Exception rethrown from confirmSend(): $e\n$s",
|
|
|
|
level: LogLevel.Info,
|
|
|
|
);
|
2024-01-08 19:40:07 +00:00
|
|
|
rethrow;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-12 17:59:07 +00:00
|
|
|
@override
|
|
|
|
Future<Amount> get availableBalance async {
|
2024-01-08 19:40:07 +00:00
|
|
|
try {
|
|
|
|
int runningBalance = 0;
|
2024-09-20 15:38:05 +00:00
|
|
|
for (final entry in cwWalletBase!.balance!.entries) {
|
2024-01-08 19:40:07 +00:00
|
|
|
runningBalance += entry.value.unlockedBalance;
|
|
|
|
}
|
|
|
|
return Amount(
|
|
|
|
rawValue: BigInt.from(runningBalance),
|
|
|
|
fractionDigits: cryptoCurrency.fractionDigits,
|
|
|
|
);
|
|
|
|
} catch (_) {
|
|
|
|
return info.cachedBalance.spendable;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-12 17:59:07 +00:00
|
|
|
@override
|
|
|
|
Future<Amount> get totalBalance async {
|
2024-01-08 19:40:07 +00:00
|
|
|
try {
|
2024-09-20 15:38:05 +00:00
|
|
|
final balanceEntries = cwWalletBase?.balance?.entries;
|
2024-01-08 19:40:07 +00:00
|
|
|
if (balanceEntries != null) {
|
|
|
|
int bal = 0;
|
2024-05-04 00:56:39 +00:00
|
|
|
for (final element in balanceEntries) {
|
2024-01-08 19:40:07 +00:00
|
|
|
bal = bal + element.value.fullBalance;
|
|
|
|
}
|
|
|
|
return Amount(
|
|
|
|
rawValue: BigInt.from(bal),
|
|
|
|
fractionDigits: cryptoCurrency.fractionDigits,
|
|
|
|
);
|
|
|
|
} else {
|
2024-09-20 15:38:05 +00:00
|
|
|
final transactions = cwWalletBase!.transactionHistory!.transactions;
|
2024-01-08 19:40:07 +00:00
|
|
|
int transactionBalance = 0;
|
2024-05-04 00:56:39 +00:00
|
|
|
for (final tx in transactions!.entries) {
|
2024-01-08 19:40:07 +00:00
|
|
|
if (tx.value.direction == TransactionDirection.incoming) {
|
|
|
|
transactionBalance += tx.value.amount!;
|
|
|
|
} else {
|
|
|
|
transactionBalance += -tx.value.amount! - tx.value.fee!;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return Amount(
|
|
|
|
rawValue: BigInt.from(transactionBalance),
|
|
|
|
fractionDigits: cryptoCurrency.fractionDigits,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch (_) {
|
|
|
|
return info.cachedBalance.total;
|
|
|
|
}
|
|
|
|
}
|
2024-05-04 00:56:27 +00:00
|
|
|
|
|
|
|
// ============== Private ====================================================
|
|
|
|
|
|
|
|
StreamSubscription<TorConnectionStatusChangedEvent>? _torStatusListener;
|
|
|
|
StreamSubscription<TorPreferenceChangedEvent>? _torPreferenceListener;
|
|
|
|
|
|
|
|
final Mutex _torConnectingLock = Mutex();
|
|
|
|
bool _requireMutex = false;
|
2024-01-08 19:40:07 +00:00
|
|
|
}
|