xmr/wow coin control

This commit is contained in:
julian 2024-09-30 09:36:12 -06:00 committed by julian-CStack
parent 7d60793c88
commit 29e63ca97d
8 changed files with 215 additions and 38 deletions

@ -1 +1 @@
Subproject commit fd9892cfa3ab85d50354f334a18a537b4af7d00e
Subproject commit 9ae2f4d1a1826ac83ef82def915e5dba0350f705

View file

@ -8,6 +8,7 @@
*
*/
import 'dart:convert';
import 'dart:math';
import 'package:isar/isar.dart';
@ -84,6 +85,20 @@ class UTXO {
return confirmations >= minimumConfirms;
}
@ignore
String? get keyImage {
if (otherData == null) {
return null;
}
try {
final map = jsonDecode(otherData!) as Map;
return map["keyImage"] as String;
} catch (_) {
return null;
}
}
UTXO copyWith({
Id? id,
String? walletId,

View file

@ -13,9 +13,9 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import '../../db/isar/main_db.dart';
import '../../models/isar/models/isar_models.dart';
import '../wallet_view/transaction_views/transaction_details_view.dart';
import '../../providers/global/wallets_provider.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/amount/amount.dart';
@ -33,6 +33,7 @@ import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/desktop/secondary_button.dart';
import '../../widgets/icon_widgets/utxo_status_icon.dart';
import '../../widgets/rounded_container.dart';
import '../wallet_view/transaction_views/transaction_details_view.dart';
class UtxoDetailsView extends ConsumerStatefulWidget {
const UtxoDetailsView({

View file

@ -7,6 +7,7 @@ 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';
import 'package:cw_core/utxo.dart' as cw;
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
@ -26,6 +27,7 @@ import 'package:tuple/tuple.dart';
import '../../../db/hive/db.dart';
import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../models/isar/models/blockchain_data/transaction.dart';
import '../../../models/isar/models/blockchain_data/utxo.dart';
import '../../../models/keys/cw_key_data.dart';
import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart';
import '../../../services/event_bus/events/global/tor_status_changed_event.dart';
@ -72,6 +74,26 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface {
await updateNode();
},
);
// 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);
});
});
}
@override
@ -614,8 +636,31 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface {
priority: feePriority,
);
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();
await prepareSendMutex.protect(() async {
awaitPendingTransaction = cwWalletBase!.createTransaction(tmp);
awaitPendingTransaction = cwWalletBase!.createTransaction(
tmp,
inputs: inputs,
);
});
} catch (e, s) {
Logging.instance.log(

View file

@ -7,6 +7,7 @@ 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';
import 'package:cw_core/utxo.dart' as cw;
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
@ -28,6 +29,7 @@ import 'package:tuple/tuple.dart';
import '../../../db/hive/db.dart';
import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../models/isar/models/blockchain_data/transaction.dart';
import '../../../models/isar/models/blockchain_data/utxo.dart';
import '../../../models/keys/cw_key_data.dart';
import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart';
import '../../../services/event_bus/events/global/tor_status_changed_event.dart';
@ -74,6 +76,26 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface {
await updateNode();
},
);
// 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);
});
});
}
@override
@ -660,8 +682,31 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface {
priority: feePriority,
);
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();
await prepareSendMutex.protect(() async {
awaitPendingTransaction = cwWalletBase!.createTransaction(tmp);
awaitPendingTransaction = cwWalletBase!.createTransaction(
tmp,
inputs: inputs,
);
});
} catch (e, s) {
Logging.instance.log(

View file

@ -1,37 +1,9 @@
import 'dart:async';
import '../../crypto_currency/intermediate/cryptonote_currency.dart';
import '../../models/tx_data.dart';
import '../wallet.dart';
import '../wallet_mixin_interfaces/coin_control_interface.dart';
import '../wallet_mixin_interfaces/mnemonic_interface.dart';
abstract class CryptonoteWallet<T extends CryptonoteCurrency> extends Wallet<T>
with MnemonicInterface<T> {
with MnemonicInterface<T>, CoinControlInterface<T> {
CryptonoteWallet(super.currency);
// ========== Overrides ======================================================
@override
Future<TxData> confirmSend({required TxData txData}) {
// TODO: implement confirmSend
throw UnimplementedError();
}
@override
Future<TxData> prepareSend({required TxData txData}) {
// TODO: implement prepareSend
throw UnimplementedError();
}
@override
Future<void> recover({required bool isRescan}) {
// TODO: implement recover
throw UnimplementedError();
}
@override
Future<bool> updateUTXOs() async {
// do nothing for now
return false;
}
}

View file

@ -1,7 +1,7 @@
import '../../crypto_currency/intermediate/bip39_hd_currency.dart';
import '../intermediate/bip39_hd_wallet.dart';
import '../../crypto_currency/crypto_currency.dart';
import '../wallet.dart';
mixin CoinControlInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
mixin CoinControlInterface<T extends CryptoCurrency> on Wallet<T> {
// any required here?
// currently only used to id which wallets support coin control
}

View file

@ -1,10 +1,12 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:cw_core/monero_transaction_priority.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/utxo.dart' as cw;
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
@ -14,6 +16,8 @@ import 'package:mutex/mutex.dart';
import '../../../models/balance.dart';
import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../models/isar/models/blockchain_data/transaction.dart';
import '../../../models/isar/models/blockchain_data/utxo.dart';
import '../../../models/keys/cw_key_data.dart';
import '../../../models/paymint/fee_object_model.dart';
import '../../../services/event_bus/events/global/blocks_remaining_event.dart';
@ -76,6 +80,44 @@ mixin CwBasedInterface<T extends CryptonoteCurrency, U extends WalletBase,
_refreshTxDataHelper();
}
final _utxosUpdateLock = Mutex();
Future<void> onUTXOsCHanged(List<UTXO> utxos) async {
await _utxosUpdateLock.protect(() async {
final cwUtxos = cwWalletBase?.utxos ?? [];
bool changed = false;
for (final cw in cwUtxos) {
final match = utxos.where(
(e) =>
e.keyImage != null &&
e.keyImage!.isNotEmpty &&
e.keyImage == cw.keyImage,
);
if (match.isNotEmpty) {
final u = match.first;
if (u.isBlocked) {
if (!cw.isFrozen) {
await cwWalletBase?.freeze(cw.keyImage);
changed = true;
}
} else {
if (cw.isFrozen) {
await cwWalletBase?.thaw(cw.keyImage);
changed = true;
}
}
}
}
if (changed) {
await cwWalletBase?.updateUTXOs();
}
});
}
void onNewTransaction() {
// TODO: [prio=low] get rid of UpdatedInBackgroundEvent and move to
// adding the v2 tx to the db which would update ui automagically since the
@ -246,7 +288,64 @@ mixin CwBasedInterface<T extends CryptonoteCurrency, U extends WalletBase,
FilterOperation? get receivingAddressFilterOperation => null;
@override
Future<void> updateBalance() async {
Future<bool> updateUTXOs() async {
await cwWalletBase?.updateUTXOs();
final List<UTXO> outputArray = [];
for (final rawUTXO in (cwWalletBase?.utxos ?? <cw.UTXO>[])) {
if (!rawUTXO.spent) {
final current = await mainDB.isar.utxos
.where()
.walletIdEqualTo(walletId)
.filter()
.voutEqualTo(rawUTXO.vout)
.and()
.txidEqualTo(rawUTXO.hash)
.findFirst();
final tx = await mainDB.isar.transactions
.where()
.walletIdEqualTo(walletId)
.filter()
.txidEqualTo(rawUTXO.hash)
.findFirst();
final otherDataMap = {
"keyImage": rawUTXO.keyImage,
"spent": rawUTXO.spent,
};
final utxo = UTXO(
address: rawUTXO.address,
walletId: walletId,
txid: rawUTXO.hash,
vout: rawUTXO.vout,
value: rawUTXO.value,
name: current?.name ?? "",
isBlocked: current?.isBlocked ?? rawUTXO.isFrozen,
blockedReason: current?.blockedReason ?? "",
isCoinbase: rawUTXO.coinbase,
blockHash: "",
blockHeight:
tx?.height ?? (rawUTXO.height > 0 ? rawUTXO.height : null),
blockTime: tx?.timestamp,
otherData: jsonEncode(otherDataMap),
);
outputArray.add(utxo);
}
}
await mainDB.updateUTXOs(walletId, outputArray);
return true;
}
@override
Future<void> updateBalance({bool shouldUpdateUtxos = true}) async {
if (shouldUpdateUtxos) {
await updateUTXOs();
}
final total = await totalBalance;
final available = await availableBalance;