mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-11-16 17:27:39 +00:00
WIP: basic full rbf
This commit is contained in:
parent
741c0be88b
commit
a566af8eb4
6 changed files with 229 additions and 45 deletions
|
@ -8,23 +8,33 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
|
||||
import '../../../../pages_desktop_specific/desktop_home_view.dart';
|
||||
import '../../../../providers/providers.dart';
|
||||
import '../../../../route_generator.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
import '../../../../utilities/amount/amount.dart';
|
||||
import '../../../../utilities/amount/amount_formatter.dart';
|
||||
import '../../../../utilities/show_loading.dart';
|
||||
import '../../../../utilities/text_styles.dart';
|
||||
import '../../../../utilities/util.dart';
|
||||
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
|
||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart';
|
||||
import '../../../../widgets/background.dart';
|
||||
import '../../../../widgets/boost_fee_slider.dart';
|
||||
import '../../../../widgets/conditional_parent.dart';
|
||||
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import '../../../../widgets/desktop/desktop_dialog.dart';
|
||||
import '../../../../widgets/desktop/primary_button.dart';
|
||||
import '../../../../widgets/detail_item.dart';
|
||||
import '../../../../widgets/fee_slider.dart';
|
||||
import '../../../../widgets/rounded_white_container.dart';
|
||||
import '../../../../widgets/stack_dialog.dart';
|
||||
import '../../../send_view/confirm_transaction_view.dart';
|
||||
|
||||
class BoostTransactionView extends ConsumerStatefulWidget {
|
||||
const BoostTransactionView({
|
||||
|
@ -47,9 +57,12 @@ class _BoostTransactionViewState extends ConsumerState<BoostTransactionView> {
|
|||
late final TransactionV2 _transaction;
|
||||
late final Amount fee;
|
||||
late final Amount amount;
|
||||
late final int rate;
|
||||
|
||||
BigInt? customFee;
|
||||
|
||||
int _newRate = 0;
|
||||
|
||||
bool _previewTxnLock = false;
|
||||
Future<void> _previewTxn() async {
|
||||
if (_previewTxnLock) {
|
||||
|
@ -57,12 +70,79 @@ class _BoostTransactionViewState extends ConsumerState<BoostTransactionView> {
|
|||
}
|
||||
_previewTxnLock = true;
|
||||
try {
|
||||
// TODO [prio=high]: define previewSend
|
||||
// build new tx and show loading/tx generation
|
||||
if (_newRate <= rate) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const StackOkDialog(
|
||||
title: "Error",
|
||||
message: "New fee rate must be greater than the current rate.",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// on success show confirm tx screen
|
||||
final wallet = (ref.read(pWallets).getWallet(walletId) as RbfInterface);
|
||||
|
||||
Exception? ex;
|
||||
// build new tx and show loading/tx generation
|
||||
final txData = await showLoading(
|
||||
whileFuture: wallet.prepareRbfSend(
|
||||
oldTransaction: _transaction,
|
||||
newRate: _newRate,
|
||||
),
|
||||
context: context,
|
||||
message: "Preparing RBF Transaction...",
|
||||
onException: (e) => ex = e,
|
||||
);
|
||||
|
||||
// on failure show error message
|
||||
if (txData == null && mounted) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "RBF send error",
|
||||
message: ex?.toString() ?? "Unknown error found",
|
||||
),
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
// on success show confirm tx screen
|
||||
if (isDesktop && mounted) {
|
||||
unawaited(
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => DesktopDialog(
|
||||
maxHeight: MediaQuery.of(context).size.height - 64,
|
||||
maxWidth: 580,
|
||||
child: ConfirmTransactionView(
|
||||
txData: txData!,
|
||||
walletId: walletId,
|
||||
onSuccess: () {},
|
||||
// isPaynymTransaction: isPaynymSend, TODO ?
|
||||
routeOnSuccessName: DesktopHomeView.routeName,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (mounted) {
|
||||
unawaited(
|
||||
Navigator.of(context).push(
|
||||
RouteGenerator.getRoute(
|
||||
shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute,
|
||||
builder: (_) => ConfirmTransactionView(
|
||||
txData: txData!,
|
||||
walletId: walletId,
|
||||
// isPaynymTransaction: isPaynymSend, TODO ?
|
||||
onSuccess: () {},
|
||||
),
|
||||
settings: const RouteSettings(
|
||||
name: ConfirmTransactionView.routeName,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
_previewTxnLock = false;
|
||||
}
|
||||
|
@ -79,6 +159,8 @@ class _BoostTransactionViewState extends ConsumerState<BoostTransactionView> {
|
|||
amount = _transaction.getAmountSentFromThisWallet(
|
||||
fractionDigits: ref.read(pWalletCoin(walletId)).fractionDigits,
|
||||
);
|
||||
rate = (fee.raw ~/ BigInt.from(_transaction.vSize!)).toInt();
|
||||
_newRate = rate + 1;
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
@ -92,9 +174,7 @@ class _BoostTransactionViewState extends ConsumerState<BoostTransactionView> {
|
|||
final String amountString = ref.watch(pAmountFormatter(coin)).format(
|
||||
amount,
|
||||
);
|
||||
final String feeRateString =
|
||||
"${(fee.raw / BigInt.from(_transaction.vSize!)).toStringAsFixed(1)}"
|
||||
" sats/vByte";
|
||||
final String feeRateString = "$rate sats/vByte";
|
||||
|
||||
return ConditionalParent(
|
||||
condition: !isDesktop,
|
||||
|
@ -155,10 +235,12 @@ class _BoostTransactionViewState extends ConsumerState<BoostTransactionView> {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
RoundedWhiteContainer(
|
||||
padding: isDesktop
|
||||
? const EdgeInsets.all(0)
|
||||
: const EdgeInsets.all(12),
|
||||
ConditionalParent(
|
||||
condition: isDesktop,
|
||||
builder: (child) => RoundedWhiteContainer(
|
||||
padding: EdgeInsets.zero,
|
||||
child: child,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
DetailItem(
|
||||
|
@ -174,38 +256,32 @@ class _BoostTransactionViewState extends ConsumerState<BoostTransactionView> {
|
|||
),
|
||||
const _Divider(),
|
||||
DetailItem(
|
||||
title: "Current fee rate",
|
||||
title: "Current rate",
|
||||
detail: feeRateString,
|
||||
horizontal: true,
|
||||
),
|
||||
const _Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: BoostFeeSlider(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: FeeSlider(
|
||||
overrideLabel: "Select a higher rate",
|
||||
onSatVByteChanged: (value) => _newRate = value,
|
||||
coin: coin,
|
||||
onFeeChanged: (fee) {
|
||||
customFee = fee;
|
||||
},
|
||||
min: _transaction
|
||||
.getFee(fractionDigits: coin.fractionDigits)
|
||||
.raw,
|
||||
max: _transaction
|
||||
.getFee(fractionDigits: coin.fractionDigits)
|
||||
.raw *
|
||||
BigInt.from(4),
|
||||
// TODO [prio=med]: The max fee should be set to an absurd fee.
|
||||
min: rate.toDouble() + 1,
|
||||
max: rate * 5.0,
|
||||
pow: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!isDesktop) const Spacer(),
|
||||
if (!isDesktop)
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
height: 16,
|
||||
),
|
||||
if (!isDesktop)
|
||||
PrimaryButton(
|
||||
buttonHeight: ButtonHeight.l,
|
||||
label: "Preview send",
|
||||
onPressed: _previewTxn,
|
||||
),
|
||||
|
|
|
@ -164,7 +164,7 @@ class _TransactionV2DetailsViewState
|
|||
unawaited(
|
||||
Navigator.of(context).pushNamed(
|
||||
BoostTransactionView.routeName,
|
||||
arguments: (tx: _transaction,),
|
||||
arguments: _transaction,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -181,7 +181,8 @@ class _TransactionV2DetailsViewState
|
|||
|
||||
if (_transaction.type
|
||||
case TransactionType.sentToSelf || TransactionType.outgoing) {
|
||||
supportsRbf = ref.read(pWallets).getWallet(walletId) is RbfInterface;
|
||||
supportsRbf = _transaction.subType == TransactionSubType.none &&
|
||||
ref.read(pWallets).getWallet(walletId) is RbfInterface;
|
||||
} else {
|
||||
supportsRbf = false;
|
||||
}
|
||||
|
@ -563,12 +564,10 @@ class _TransactionV2DetailsViewState
|
|||
outputLabel = "Sent to";
|
||||
}
|
||||
|
||||
// TODO: [prio=high]: revert the following when done testing
|
||||
final confirmedTxn = false;
|
||||
// final confirmedTxn = _transaction.isConfirmed(
|
||||
// currentHeight,
|
||||
// coin.minConfirms,
|
||||
// );
|
||||
final confirmedTxn = _transaction.isConfirmed(
|
||||
currentHeight,
|
||||
coin.minConfirms,
|
||||
);
|
||||
|
||||
return ConditionalParent(
|
||||
condition: !isDesktop,
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import 'package:cw_monero/pending_monero_transaction.dart';
|
||||
import 'package:cw_wownero/pending_wownero_transaction.dart';
|
||||
import 'package:tezart/tezart.dart' as tezart;
|
||||
import 'package:web3dart/web3dart.dart' as web3dart;
|
||||
|
||||
import '../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
|
||||
import '../../models/isar/models/isar_models.dart';
|
||||
import '../../models/paynym/paynym_account_lite.dart';
|
||||
import '../../utilities/amount/amount.dart';
|
||||
import '../../utilities/enums/fee_rate_type_enum.dart';
|
||||
import '../isar/models/spark_coin.dart';
|
||||
import 'package:tezart/tezart.dart' as tezart;
|
||||
import 'package:web3dart/web3dart.dart' as web3dart;
|
||||
|
||||
class TxData {
|
||||
final FeeRateType? feeRateType;
|
||||
|
@ -76,6 +77,8 @@ class TxData {
|
|||
|
||||
final TransactionV2? tempTx;
|
||||
|
||||
final bool ignoreCachedBalanceChecks;
|
||||
|
||||
TxData({
|
||||
this.feeRateType,
|
||||
this.feeRateAmount,
|
||||
|
@ -112,6 +115,7 @@ class TxData {
|
|||
this.sparkMints,
|
||||
this.usedSparkCoins,
|
||||
this.tempTx,
|
||||
this.ignoreCachedBalanceChecks = false,
|
||||
});
|
||||
|
||||
Amount? get amount => recipients != null && recipients!.isNotEmpty
|
||||
|
@ -196,6 +200,7 @@ class TxData {
|
|||
List<TxData>? sparkMints,
|
||||
List<SparkCoin>? usedSparkCoins,
|
||||
TransactionV2? tempTx,
|
||||
bool? ignoreCachedBalanceChecks,
|
||||
}) {
|
||||
return TxData(
|
||||
feeRateType: feeRateType ?? this.feeRateType,
|
||||
|
@ -235,6 +240,8 @@ class TxData {
|
|||
sparkMints: sparkMints ?? this.sparkMints,
|
||||
usedSparkCoins: usedSparkCoins ?? this.usedSparkCoins,
|
||||
tempTx: tempTx ?? this.tempTx,
|
||||
ignoreCachedBalanceChecks:
|
||||
ignoreCachedBalanceChecks ?? this.ignoreCachedBalanceChecks,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -274,5 +281,6 @@ class TxData {
|
|||
'sparkMints: $sparkMints, '
|
||||
'usedSparkCoins: $usedSparkCoins, '
|
||||
'tempTx: $tempTx, '
|
||||
'ignoreCachedBalanceChecks: $ignoreCachedBalanceChecks, '
|
||||
'}';
|
||||
}
|
||||
|
|
|
@ -1648,7 +1648,8 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
if (customSatsPerVByte != null) {
|
||||
// check for send all
|
||||
bool isSendAll = false;
|
||||
if (txData.amount == info.cachedBalance.spendable) {
|
||||
if (txData.ignoreCachedBalanceChecks ||
|
||||
txData.amount == info.cachedBalance.spendable) {
|
||||
isSendAll = true;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
|
||||
import '../../../models/isar/models/isar_models.dart';
|
||||
import '../../../utilities/amount/amount.dart';
|
||||
import '../../../utilities/enums/fee_rate_type_enum.dart';
|
||||
import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
|
||||
import '../../models/tx_data.dart';
|
||||
import 'electrumx_interface.dart';
|
||||
|
||||
typedef TxSize = ({int real, int virtual});
|
||||
|
@ -31,5 +37,83 @@ mixin RbfInterface<T extends ElectrumXCurrencyInterface>
|
|||
return updatedTx;
|
||||
}
|
||||
|
||||
// TODO more RBF specific logic
|
||||
Future<TxData> prepareRbfSend({
|
||||
required TransactionV2 oldTransaction,
|
||||
required int newRate,
|
||||
}) async {
|
||||
final Set<UTXO> utxos = {};
|
||||
for (final input in oldTransaction.inputs) {
|
||||
final utxo = UTXO(
|
||||
walletId: walletId,
|
||||
txid: input.outpoint!.txid,
|
||||
vout: input.outpoint!.vout,
|
||||
value: input.value.toInt(),
|
||||
name: "rbf",
|
||||
isBlocked: false,
|
||||
blockedReason: null,
|
||||
isCoinbase: false,
|
||||
blockHash: "rbf",
|
||||
blockHeight: 1,
|
||||
blockTime: 1,
|
||||
used: false,
|
||||
address: input.addresses.first,
|
||||
);
|
||||
|
||||
utxos.add(utxo);
|
||||
}
|
||||
|
||||
Amount sendAmount = oldTransaction.getAmountSentFromThisWallet(
|
||||
fractionDigits: cryptoCurrency.fractionDigits,
|
||||
);
|
||||
|
||||
// TODO: fix fragile firstWhere (or at least add some error checking)
|
||||
final address = oldTransaction.outputs
|
||||
.firstWhere(
|
||||
(e) => e.value == sendAmount.raw,
|
||||
)
|
||||
.addresses
|
||||
.first;
|
||||
|
||||
final inSum = utxos
|
||||
.map((e) => BigInt.from(e.value))
|
||||
.fold(BigInt.zero, (p, e) => p + e);
|
||||
|
||||
if (oldTransaction
|
||||
.getFee(fractionDigits: cryptoCurrency.fractionDigits)
|
||||
.raw +
|
||||
sendAmount.raw ==
|
||||
inSum) {
|
||||
sendAmount = Amount(
|
||||
rawValue: oldTransaction
|
||||
.getFee(fractionDigits: cryptoCurrency.fractionDigits)
|
||||
.raw +
|
||||
sendAmount.raw,
|
||||
fractionDigits: cryptoCurrency.fractionDigits,
|
||||
);
|
||||
}
|
||||
|
||||
final note = await mainDB.isar.transactionNotes
|
||||
.where()
|
||||
.walletIdEqualTo(walletId)
|
||||
.filter()
|
||||
.txidEqualTo(oldTransaction.txid)
|
||||
.findFirst();
|
||||
|
||||
final txData = TxData(
|
||||
recipients: [
|
||||
(
|
||||
address: address,
|
||||
amount: sendAmount,
|
||||
isChange: false,
|
||||
),
|
||||
],
|
||||
feeRateType: FeeRateType.custom,
|
||||
satsPerVByte: newRate,
|
||||
utxos: utxos,
|
||||
ignoreCachedBalanceChecks: true,
|
||||
note: note?.value ?? "",
|
||||
);
|
||||
|
||||
return await prepareSend(txData: txData);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +1,45 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../utilities/text_styles.dart';
|
||||
import '../wallets/crypto_currency/crypto_currency.dart';
|
||||
|
||||
/// This has limitations. At least one of [pow] or [min] must be set to 1
|
||||
class FeeSlider extends StatefulWidget {
|
||||
const FeeSlider({
|
||||
super.key,
|
||||
required this.onSatVByteChanged,
|
||||
required this.coin,
|
||||
this.min = 1,
|
||||
this.max = 5,
|
||||
this.pow = 4,
|
||||
this.showWU = false,
|
||||
this.overrideLabel,
|
||||
});
|
||||
|
||||
final CryptoCurrency coin;
|
||||
final double min;
|
||||
final double max;
|
||||
final double pow;
|
||||
final bool showWU;
|
||||
final void Function(int) onSatVByteChanged;
|
||||
final String? overrideLabel;
|
||||
|
||||
@override
|
||||
State<FeeSlider> createState() => _FeeSliderState();
|
||||
}
|
||||
|
||||
class _FeeSliderState extends State<FeeSlider> {
|
||||
static const double min = 1;
|
||||
static const double max = 4;
|
||||
|
||||
double sliderValue = 0;
|
||||
|
||||
int rate = min.toInt();
|
||||
late int rate;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
rate = widget.min.toInt();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -36,7 +49,7 @@ class _FeeSliderState extends State<FeeSlider> {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
widget.showWU ? "sat/WU" : "sat/vByte",
|
||||
widget.overrideLabel ?? (widget.showWU ? "sat/WU" : "sat/vByte"),
|
||||
style: STextStyles.smallMed12(context),
|
||||
),
|
||||
Text(
|
||||
|
@ -50,7 +63,10 @@ class _FeeSliderState extends State<FeeSlider> {
|
|||
onChanged: (value) {
|
||||
setState(() {
|
||||
sliderValue = value;
|
||||
final number = pow(sliderValue * (max - min) + min, 4).toDouble();
|
||||
final number = pow(
|
||||
sliderValue * (widget.max - widget.min) + widget.min,
|
||||
widget.pow,
|
||||
).toDouble();
|
||||
if (widget.coin is Dogecoin) {
|
||||
rate = (number * 1000).toInt();
|
||||
} else {
|
||||
|
|
Loading…
Reference in a new issue