Skip to content

Commit

Permalink
add ability to "send all" selected UTXOs when using coin control
Browse files Browse the repository at this point in the history
  • Loading branch information
julian-CStack committed Aug 28, 2024
1 parent 02ab5b0 commit 10354b2
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 103 deletions.
98 changes: 45 additions & 53 deletions lib/pages/send_view/send_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,48 @@ class _SendViewState extends ConsumerState<SendView> {
}
}

String _getSendAllTitle(bool showCoinControl, Set<UTXO> selectedUTXOs) {
if (showCoinControl && selectedUTXOs.isNotEmpty) {
return "Send all selected";
}

return "Send all ${coin.ticker}";
}

Amount _selectedUtxosAmount(Set<UTXO> utxos) => Amount(
rawValue:
utxos.map((e) => BigInt.from(e.value)).reduce((v, e) => v += e),
fractionDigits: ref.read(pWalletCoin(walletId)).fractionDigits,
);

Future<void> _sendAllTapped(bool showCoinControl) async {
final Amount amount;

if (showCoinControl && selectedUTXOs.isNotEmpty) {
amount = _selectedUtxosAmount(selectedUTXOs);
} else if (isFiro) {
switch (ref.read(publicPrivateBalanceStateProvider.state).state) {
case FiroType.public:
amount = ref.read(pWalletBalance(walletId)).spendable;
break;
case FiroType.lelantus:
amount = ref.read(pWalletBalanceSecondary(walletId)).spendable;
break;
case FiroType.spark:
amount = ref.read(pWalletBalanceTertiary(walletId)).spendable;
break;
}
} else {
amount = ref.read(pWalletBalance(walletId)).spendable;
}

cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format(
amount,
withUnitName: false,
);
_cryptoAmountChanged();
}

bool get isPaynymSend => widget.accountLite != null;

bool isCustomFee = false;
Expand Down Expand Up @@ -1772,59 +1814,9 @@ class _SendViewState extends ConsumerState<SendView> {
),
if (coin is! Ethereum && coin is! Tezos)
CustomTextButton(
text: "Send all ${coin.ticker}",
onTap: () async {
if (isFiro) {
final Amount amount;
switch (ref
.read(
publicPrivateBalanceStateProvider
.state,
)
.state) {
case FiroType.public:
amount = ref
.read(pWalletBalance(walletId))
.spendable;
break;
case FiroType.lelantus:
amount = ref
.read(
pWalletBalanceSecondary(
walletId,
),
)
.spendable;
break;
case FiroType.spark:
amount = ref
.read(
pWalletBalanceTertiary(
walletId,
),
)
.spendable;
break;
}

cryptoAmountController.text = ref
.read(pAmountFormatter(coin))
.format(
amount,
withUnitName: false,
);
} else {
cryptoAmountController.text = ref
.read(pAmountFormatter(coin))
.format(
ref
.read(pWalletBalance(walletId))
.spendable,
withUnitName: false,
);
}
_cryptoAmountChanged();
},
text: _getSendAllTitle(
showCoinControl, selectedUTXOs),
onTap: () => _sendAllTapped(showCoinControl),
),
],
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';

import '../../../../models/isar/models/blockchain_data/utxo.dart';
import '../../../../models/isar/models/contact_entry.dart';
import '../../../../models/paynym/paynym_account_lite.dart';
import '../../../../models/send_view_auto_fill_data.dart';
Expand Down Expand Up @@ -932,30 +933,45 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
ref.read(pSendAmount.notifier).state = amount;
}

Future<void> sendAllTapped() async {
final info = ref.read(pWalletInfo(walletId));
String _getSendAllTitle(bool showCoinControl, Set<UTXO> selectedUTXOs) {
if (showCoinControl && selectedUTXOs.isNotEmpty) {
return "Send all selected";
}

return "Send all ${coin.ticker}";
}

Amount _selectedUtxosAmount(Set<UTXO> utxos) => Amount(
rawValue:
utxos.map((e) => BigInt.from(e.value)).reduce((v, e) => v += e),
fractionDigits: ref.read(pWalletCoin(walletId)).fractionDigits,
);

if (coin is Firo) {
Future<void> _sendAllTapped(bool showCoinControl) async {
final Amount amount;

if (showCoinControl && ref.read(desktopUseUTXOs).isNotEmpty) {
amount = _selectedUtxosAmount(ref.read(desktopUseUTXOs));
} else if (coin is Firo) {
switch (ref.read(publicPrivateBalanceStateProvider.state).state) {
case FiroType.public:
cryptoAmountController.text = info.cachedBalance.spendable.decimal
.toStringAsFixed(coin.fractionDigits);
amount = ref.read(pWalletBalance(walletId)).spendable;
break;
case FiroType.lelantus:
cryptoAmountController.text = info
.cachedBalanceSecondary.spendable.decimal
.toStringAsFixed(coin.fractionDigits);
amount = ref.read(pWalletBalanceSecondary(walletId)).spendable;
break;
case FiroType.spark:
cryptoAmountController.text = info
.cachedBalanceTertiary.spendable.decimal
.toStringAsFixed(coin.fractionDigits);
amount = ref.read(pWalletBalanceTertiary(walletId)).spendable;
break;
}
} else {
cryptoAmountController.text = info.cachedBalance.spendable.decimal
.toStringAsFixed(coin.fractionDigits);
amount = ref.read(pWalletBalance(walletId)).spendable;
}

cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format(
amount,
withUnitName: false,
);
}

void _showDesktopCoinControl() async {
Expand Down Expand Up @@ -1280,8 +1296,11 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
),
if (coin is! Ethereum && coin is! Tezos)
CustomTextButton(
text: "Send all ${coin.ticker}",
onTap: sendAllTapped,
text: _getSendAllTitle(
showCoinControl,
ref.watch(desktopUseUTXOs),
),
onTap: () => _sendAllTapped(showCoinControl),
),
],
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
required TxData txData,
required bool coinControl,
required bool isSendAll,
required bool isSendAllCoinControlUtxos,
int additionalOutputs = 0,
List<UTXO>? utxos,
}) async {
Expand Down Expand Up @@ -144,7 +145,9 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>

if (spendableSatoshiValue < satoshiAmountToSend) {
throw Exception("Insufficient balance");
} else if (spendableSatoshiValue == satoshiAmountToSend && !isSendAll) {
} else if (spendableSatoshiValue == satoshiAmountToSend &&
!isSendAll &&
!isSendAllCoinControlUtxos) {
throw Exception("Insufficient balance to pay transaction fee");
}

Expand Down Expand Up @@ -220,7 +223,8 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
// gather required signing data
final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse);

if (isSendAll) {
if (isSendAll || isSendAllCoinControlUtxos) {
assert(satoshiAmountToSend == satoshisBeingUsed);
return await _sendAllBuilder(
txData: txData,
recipientAddress: recipientAddress,
Expand Down Expand Up @@ -357,6 +361,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
additionalOutputs: additionalOutputs + 1,
utxos: utxos,
coinControl: coinControl,
isSendAllCoinControlUtxos: isSendAllCoinControlUtxos,
);
}
throw Exception("Insufficient balance to pay transaction fee");
Expand Down Expand Up @@ -1681,11 +1686,23 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
@override
Future<TxData> prepareSend({required TxData txData}) async {
try {
if (txData.amount == null) {
throw Exception("No recipients in attempted transaction!");
}

final feeRateType = txData.feeRateType;
final customSatsPerVByte = txData.satsPerVByte;
final feeRateAmount = txData.feeRateAmount;
final utxos = txData.utxos;

final bool coinControl = utxos != null;

final isSendAllCoinControlUtxos = coinControl &&
txData.amount!.raw ==
utxos
.map((e) => e.value)
.fold(BigInt.zero, (p, e) => p + BigInt.from(e));

if (customSatsPerVByte != null) {
// check for send all
bool isSendAll = false;
Expand All @@ -1694,8 +1711,6 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
isSendAll = true;
}

final bool coinControl = utxos != null;

if (coinControl &&
this is CpfpInterface &&
txData.amount ==
Expand All @@ -1709,6 +1724,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
isSendAll: isSendAll,
utxos: utxos?.toList(),
coinControl: coinControl,
isSendAllCoinControlUtxos: isSendAllCoinControlUtxos,
);

Logging.instance
Expand Down Expand Up @@ -1750,15 +1766,14 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
isSendAll = true;
}

final bool coinControl = utxos != null;

final result = await coinSelection(
txData: txData.copyWith(
feeRateAmount: rate,
),
isSendAll: isSendAll,
utxos: utxos?.toList(),
coinControl: coinControl,
isSendAllCoinControlUtxos: isSendAllCoinControlUtxos,
);

Logging.instance.log("prepare send: $result", level: LogLevel.Info);
Expand Down
69 changes: 40 additions & 29 deletions lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import '../../isar/models/spark_coin.dart';
import '../../isar/models/wallet_info.dart';
import '../../models/tx_data.dart';
import '../intermediate/bip39_hd_wallet.dart';
import 'cpfp_interface.dart';
import 'electrumx_interface.dart';

const kDefaultSparkIndex = 1;
Expand Down Expand Up @@ -1809,36 +1810,44 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
throw Exception("Attempted send of zero amount");
}

final currentHeight = await chainHeight;
final utxos = txData.utxos;
final bool coinControl = utxos != null;

// coin control not enabled for firo currently so we can ignore this
// final utxosToUse = txData.utxos?.toList() ?? await mainDB.isar.utxos
// .where()
// .walletIdEqualTo(walletId)
// .filter()
// .isBlockedEqualTo(false)
// .and()
// .group((q) => q.usedEqualTo(false).or().usedIsNull())
// .and()
// .valueGreaterThan(0)
// .findAll();
final spendableUtxos = await mainDB.isar.utxos
.where()
.walletIdEqualTo(walletId)
.filter()
.isBlockedEqualTo(false)
.and()
.group((q) => q.usedEqualTo(false).or().usedIsNull())
.and()
.valueGreaterThan(0)
.findAll();
final utxosTotal = coinControl
? utxos
.map((e) => e.value)
.fold(BigInt.zero, (p, e) => p + BigInt.from(e))
: null;

spendableUtxos.removeWhere(
(e) => !e.isConfirmed(
currentHeight,
cryptoCurrency.minConfirms,
),
);
if (coinControl && utxosTotal! < total) {
throw Exception("Insufficient selected UTXOs!");
}

final isSendAllCoinControlUtxos = coinControl && total == utxosTotal;

final currentHeight = await chainHeight;

final availableOutputs = utxos?.toList() ??
await mainDB.isar.utxos
.where()
.walletIdEqualTo(walletId)
.filter()
.isBlockedEqualTo(false)
.and()
.group((q) => q.usedEqualTo(false).or().usedIsNull())
.and()
.valueGreaterThan(0)
.findAll();

final canCPFP = this is CpfpInterface && coinControl;

final spendableUtxos = availableOutputs
.where(
(e) =>
canCPFP ||
e.isConfirmed(currentHeight, cryptoCurrency.minConfirms),
)
.toList();

if (spendableUtxos.isEmpty) {
throw Exception("No available UTXOs found to anonymize");
Expand All @@ -1849,7 +1858,9 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
.reduce((value, element) => value += element);

final bool subtractFeeFromAmount;
if (available < total) {
if (isSendAllCoinControlUtxos) {
subtractFeeFromAmount = true;
} else if (available < total) {
throw Exception("Insufficient balance");
} else if (available == total) {
subtractFeeFromAmount = true;
Expand Down

0 comments on commit 10354b2

Please sign in to comment.