Skip to content

Commit

Permalink
xmr/wow coin control
Browse files Browse the repository at this point in the history
  • Loading branch information
julian-CStack committed Sep 30, 2024
1 parent e052b57 commit 9deb80f
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 38 deletions.
15 changes: 15 additions & 0 deletions lib/models/isar/models/blockchain_data/utxo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*
*/

import 'dart:convert';
import 'dart:math';

import 'package:isar/isar.dart';
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion lib/pages/coin_control/utxo_details_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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({
Expand Down
47 changes: 46 additions & 1 deletion lib/wallets/wallet/impl/monero_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
47 changes: 46 additions & 1 deletion lib/wallets/wallet/impl/wownero_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
32 changes: 2 additions & 30 deletions lib/wallets/wallet/intermediate/cryptonote_wallet.dart
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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
}
101 changes: 100 additions & 1 deletion lib/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand Down

0 comments on commit 9deb80f

Please sign in to comment.