mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-03 09:19:22 +00:00
xmr/wow coin control
This commit is contained in:
parent
7d60793c88
commit
29e63ca97d
8 changed files with 215 additions and 38 deletions
|
@ -1 +1 @@
|
||||||
Subproject commit fd9892cfa3ab85d50354f334a18a537b4af7d00e
|
Subproject commit 9ae2f4d1a1826ac83ef82def915e5dba0350f705
|
|
@ -8,6 +8,7 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
@ -84,6 +85,20 @@ class UTXO {
|
||||||
return confirmations >= minimumConfirms;
|
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({
|
UTXO copyWith({
|
||||||
Id? id,
|
Id? id,
|
||||||
String? walletId,
|
String? walletId,
|
||||||
|
|
|
@ -13,9 +13,9 @@ import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
import '../../db/isar/main_db.dart';
|
import '../../db/isar/main_db.dart';
|
||||||
import '../../models/isar/models/isar_models.dart';
|
import '../../models/isar/models/isar_models.dart';
|
||||||
import '../wallet_view/transaction_views/transaction_details_view.dart';
|
|
||||||
import '../../providers/global/wallets_provider.dart';
|
import '../../providers/global/wallets_provider.dart';
|
||||||
import '../../themes/stack_colors.dart';
|
import '../../themes/stack_colors.dart';
|
||||||
import '../../utilities/amount/amount.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/desktop/secondary_button.dart';
|
||||||
import '../../widgets/icon_widgets/utxo_status_icon.dart';
|
import '../../widgets/icon_widgets/utxo_status_icon.dart';
|
||||||
import '../../widgets/rounded_container.dart';
|
import '../../widgets/rounded_container.dart';
|
||||||
|
import '../wallet_view/transaction_views/transaction_details_view.dart';
|
||||||
|
|
||||||
class UtxoDetailsView extends ConsumerStatefulWidget {
|
class UtxoDetailsView extends ConsumerStatefulWidget {
|
||||||
const UtxoDetailsView({
|
const UtxoDetailsView({
|
||||||
|
|
|
@ -7,6 +7,7 @@ import 'package:cw_core/node.dart';
|
||||||
import 'package:cw_core/pending_transaction.dart';
|
import 'package:cw_core/pending_transaction.dart';
|
||||||
import 'package:cw_core/sync_status.dart';
|
import 'package:cw_core/sync_status.dart';
|
||||||
import 'package:cw_core/transaction_direction.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_base.dart';
|
||||||
import 'package:cw_core/wallet_credentials.dart';
|
import 'package:cw_core/wallet_credentials.dart';
|
||||||
import 'package:cw_core/wallet_info.dart';
|
import 'package:cw_core/wallet_info.dart';
|
||||||
|
@ -26,6 +27,7 @@ import 'package:tuple/tuple.dart';
|
||||||
import '../../../db/hive/db.dart';
|
import '../../../db/hive/db.dart';
|
||||||
import '../../../models/isar/models/blockchain_data/address.dart';
|
import '../../../models/isar/models/blockchain_data/address.dart';
|
||||||
import '../../../models/isar/models/blockchain_data/transaction.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/keys/cw_key_data.dart';
|
||||||
import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart';
|
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/events/global/tor_status_changed_event.dart';
|
||||||
|
@ -72,6 +74,26 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface {
|
||||||
await updateNode();
|
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
|
@override
|
||||||
|
@ -614,8 +636,31 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface {
|
||||||
priority: feePriority,
|
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 {
|
await prepareSendMutex.protect(() async {
|
||||||
awaitPendingTransaction = cwWalletBase!.createTransaction(tmp);
|
awaitPendingTransaction = cwWalletBase!.createTransaction(
|
||||||
|
tmp,
|
||||||
|
inputs: inputs,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log(
|
Logging.instance.log(
|
||||||
|
|
|
@ -7,6 +7,7 @@ import 'package:cw_core/node.dart';
|
||||||
import 'package:cw_core/pending_transaction.dart';
|
import 'package:cw_core/pending_transaction.dart';
|
||||||
import 'package:cw_core/sync_status.dart';
|
import 'package:cw_core/sync_status.dart';
|
||||||
import 'package:cw_core/transaction_direction.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_base.dart';
|
||||||
import 'package:cw_core/wallet_credentials.dart';
|
import 'package:cw_core/wallet_credentials.dart';
|
||||||
import 'package:cw_core/wallet_info.dart';
|
import 'package:cw_core/wallet_info.dart';
|
||||||
|
@ -28,6 +29,7 @@ import 'package:tuple/tuple.dart';
|
||||||
import '../../../db/hive/db.dart';
|
import '../../../db/hive/db.dart';
|
||||||
import '../../../models/isar/models/blockchain_data/address.dart';
|
import '../../../models/isar/models/blockchain_data/address.dart';
|
||||||
import '../../../models/isar/models/blockchain_data/transaction.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/keys/cw_key_data.dart';
|
||||||
import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart';
|
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/events/global/tor_status_changed_event.dart';
|
||||||
|
@ -74,6 +76,26 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface {
|
||||||
await updateNode();
|
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
|
@override
|
||||||
|
@ -660,8 +682,31 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface {
|
||||||
priority: feePriority,
|
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 {
|
await prepareSendMutex.protect(() async {
|
||||||
awaitPendingTransaction = cwWalletBase!.createTransaction(tmp);
|
awaitPendingTransaction = cwWalletBase!.createTransaction(
|
||||||
|
tmp,
|
||||||
|
inputs: inputs,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Logging.instance.log(
|
Logging.instance.log(
|
||||||
|
|
|
@ -1,37 +1,9 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import '../../crypto_currency/intermediate/cryptonote_currency.dart';
|
import '../../crypto_currency/intermediate/cryptonote_currency.dart';
|
||||||
import '../../models/tx_data.dart';
|
|
||||||
import '../wallet.dart';
|
import '../wallet.dart';
|
||||||
|
import '../wallet_mixin_interfaces/coin_control_interface.dart';
|
||||||
import '../wallet_mixin_interfaces/mnemonic_interface.dart';
|
import '../wallet_mixin_interfaces/mnemonic_interface.dart';
|
||||||
|
|
||||||
abstract class CryptonoteWallet<T extends CryptonoteCurrency> extends Wallet<T>
|
abstract class CryptonoteWallet<T extends CryptonoteCurrency> extends Wallet<T>
|
||||||
with MnemonicInterface<T> {
|
with MnemonicInterface<T>, CoinControlInterface<T> {
|
||||||
CryptonoteWallet(super.currency);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import '../../crypto_currency/intermediate/bip39_hd_currency.dart';
|
import '../../crypto_currency/crypto_currency.dart';
|
||||||
import '../intermediate/bip39_hd_wallet.dart';
|
import '../wallet.dart';
|
||||||
|
|
||||||
mixin CoinControlInterface<T extends Bip39HDCurrency> on Bip39HDWallet<T> {
|
mixin CoinControlInterface<T extends CryptoCurrency> on Wallet<T> {
|
||||||
// any required here?
|
// any required here?
|
||||||
// currently only used to id which wallets support coin control
|
// currently only used to id which wallets support coin control
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:cw_core/monero_transaction_priority.dart';
|
import 'package:cw_core/monero_transaction_priority.dart';
|
||||||
import 'package:cw_core/sync_status.dart';
|
import 'package:cw_core/sync_status.dart';
|
||||||
import 'package:cw_core/transaction_direction.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_base.dart';
|
||||||
import 'package:cw_core/wallet_service.dart';
|
import 'package:cw_core/wallet_service.dart';
|
||||||
import 'package:cw_core/wallet_type.dart';
|
import 'package:cw_core/wallet_type.dart';
|
||||||
|
@ -14,6 +16,8 @@ import 'package:mutex/mutex.dart';
|
||||||
|
|
||||||
import '../../../models/balance.dart';
|
import '../../../models/balance.dart';
|
||||||
import '../../../models/isar/models/blockchain_data/address.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/keys/cw_key_data.dart';
|
||||||
import '../../../models/paymint/fee_object_model.dart';
|
import '../../../models/paymint/fee_object_model.dart';
|
||||||
import '../../../services/event_bus/events/global/blocks_remaining_event.dart';
|
import '../../../services/event_bus/events/global/blocks_remaining_event.dart';
|
||||||
|
@ -76,6 +80,44 @@ mixin CwBasedInterface<T extends CryptonoteCurrency, U extends WalletBase,
|
||||||
_refreshTxDataHelper();
|
_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() {
|
void onNewTransaction() {
|
||||||
// TODO: [prio=low] get rid of UpdatedInBackgroundEvent and move to
|
// TODO: [prio=low] get rid of UpdatedInBackgroundEvent and move to
|
||||||
// adding the v2 tx to the db which would update ui automagically since the
|
// 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;
|
FilterOperation? get receivingAddressFilterOperation => null;
|
||||||
|
|
||||||
@override
|
@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 total = await totalBalance;
|
||||||
final available = await availableBalance;
|
final available = await availableBalance;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue