From 88df57177b27e75a17e02ec07c96cedf899458d5 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 7 Sep 2022 10:58:54 -0600 Subject: [PATCH] public firo fee estimate and firo fee ui updates --- lib/pages/send_view/send_view.dart | 272 ++++++++++++------ .../transaction_fee_selection_sheet.dart | 63 +++- lib/services/coins/firo/firo_wallet.dart | 64 +++++ 3 files changed, 301 insertions(+), 98 deletions(-) diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 89ec70866..6dc274464 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -167,13 +167,26 @@ class _SendViewState extends ConsumerState { late Future _calculateFeesFuture; Map cachedFees = {}; + Map cachedFiroPrivateFees = {}; + Map cachedFiroPublicFees = {}; Future calculateFees(int amount) async { if (amount <= 0) { return "0"; } - if (cachedFees[amount] != null) { + if (coin == Coin.firo || coin == Coin.firoTestNet) { + if (ref.read(publicPrivateBalanceStateProvider.state).state == + "Private") { + if (cachedFiroPrivateFees[amount] != null) { + return cachedFiroPrivateFees[amount]!; + } + } else { + if (cachedFiroPublicFees[amount] != null) { + return cachedFiroPublicFees[amount]!; + } + } + } else if (cachedFees[amount] != null) { return cachedFees[amount]!; } @@ -195,12 +208,33 @@ class _SendViewState extends ConsumerState { break; } - final fee = await manager.estimateFeeFor(amount, feeRate); + int fee; - cachedFees[amount] = - Format.satoshisToAmount(fee).toStringAsFixed(Constants.decimalPlaces); + if (coin == Coin.firo || coin == Coin.firoTestNet) { + if (ref.read(publicPrivateBalanceStateProvider.state).state == + "Private") { + fee = await manager.estimateFeeFor(amount, feeRate); - return cachedFees[amount]!; + cachedFiroPrivateFees[amount] = Format.satoshisToAmount(fee) + .toStringAsFixed(Constants.decimalPlaces); + + return cachedFiroPrivateFees[amount]!; + } else { + fee = await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate); + + cachedFiroPublicFees[amount] = Format.satoshisToAmount(fee) + .toStringAsFixed(Constants.decimalPlaces); + + return cachedFiroPublicFees[amount]!; + } + } else { + fee = await manager.estimateFeeFor(amount, feeRate); + cachedFees[amount] = + Format.satoshisToAmount(fee).toStringAsFixed(Constants.decimalPlaces); + + return cachedFees[amount]!; + } } Future _firoBalanceFuture( @@ -309,6 +343,22 @@ class _SendViewState extends ConsumerState { .select((value) => value.getManagerProvider(walletId))); final String locale = ref.watch( localeServiceChangeNotifierProvider.select((value) => value.locale)); + + if (coin == Coin.firo || coin == Coin.firoTestNet) { + ref.listen(publicPrivateBalanceStateProvider, (previous, next) { + if (_amountToSend == null) { + setState(() { + _calculateFeesFuture = calculateFees(0); + }); + } else { + setState(() { + _calculateFeesFuture = + calculateFees(Format.decimalAmountToSatoshis(_amountToSend!)); + }); + } + }); + } + return Scaffold( backgroundColor: CFColors.almostWhite, appBar: AppBar( @@ -1200,74 +1250,126 @@ class _SendViewState extends ConsumerState { Constants.size.circularBorderRadius, ), ), - onPressed: () { - showModalBottomSheet( - backgroundColor: Colors.transparent, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), + onPressed: (coin == Coin.firo || + coin == Coin.firoTestNet) && + ref + .watch( + publicPrivateBalanceStateProvider + .state) + .state == + "Private" + ? null + : () { + showModalBottomSheet( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => + TransactionFeeSelectionSheet( + walletId: walletId, + amount: Decimal.tryParse( + cryptoAmountController + .text) ?? + Decimal.zero, + ), + ); + }, + child: ((coin == Coin.firo || + coin == Coin.firoTestNet) && + ref + .watch( + publicPrivateBalanceStateProvider + .state) + .state == + "Private") + ? Row( + children: [ + FutureBuilder( + future: _calculateFeesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + return Text( + "~${snapshot.data! as String} ${coin.ticker}", + style: + STextStyles.itemSubtitle, + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Calculating", + "Calculating.", + "Calculating..", + "Calculating...", + ], + style: + STextStyles.itemSubtitle, + ); + } + }, + ), + ], + ) + : Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + ref + .watch( + feeRateTypeStateProvider + .state) + .state + .prettyName, + style: + STextStyles.itemSubtitle12, + ), + const SizedBox( + width: 10, + ), + FutureBuilder( + future: _calculateFeesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + return Text( + "~${snapshot.data! as String} ${coin.ticker}", + style: STextStyles + .itemSubtitle, + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Calculating", + "Calculating.", + "Calculating..", + "Calculating...", + ], + style: STextStyles + .itemSubtitle, + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: CFColors.gray3, + ), + ], ), - ), - builder: (_) => - TransactionFeeSelectionSheet( - walletId: walletId, - amount: Decimal.tryParse( - cryptoAmountController.text) ?? - Decimal.zero, - ), - ); - }, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Text( - ref - .watch(feeRateTypeStateProvider - .state) - .state - .prettyName, - style: STextStyles.itemSubtitle12, - ), - const SizedBox( - width: 10, - ), - FutureBuilder( - future: _calculateFeesFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - return Text( - "~${snapshot.data! as String} ${coin.ticker}", - style: STextStyles.itemSubtitle, - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Calculating", - "Calculating.", - "Calculating..", - "Calculating...", - ], - style: STextStyles.itemSubtitle, - ); - } - }, - ), - ], - ), - SvgPicture.asset( - Assets.svg.chevronDown, - width: 8, - height: 4, - color: CFColors.gray3, - ), - ], - ), ), ) ], @@ -1335,21 +1437,25 @@ class _SendViewState extends ConsumerState { _amountToSend!); int availableBalance; if ((coin == Coin.firo || - coin == Coin.firoTestNet) - ) { + coin == Coin.firoTestNet)) { if (ref - .read( - publicPrivateBalanceStateProvider - .state) - .state == + .read( + publicPrivateBalanceStateProvider + .state) + .state == "Private") { - availableBalance = Format.decimalAmountToSatoshis( - await (manager.wallet as FiroWallet).availablePrivateBalance()); + availableBalance = + Format.decimalAmountToSatoshis( + await (manager.wallet + as FiroWallet) + .availablePrivateBalance()); } else { - availableBalance = Format.decimalAmountToSatoshis( - await (manager.wallet as FiroWallet).availablePublicBalance()); + availableBalance = + Format.decimalAmountToSatoshis( + await (manager.wallet + as FiroWallet) + .availablePublicBalance()); } - } else { availableBalance = Format.decimalAmountToSatoshis( diff --git a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart index 9a214bfab..4eca34f3d 100644 --- a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart @@ -4,6 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; +import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; +import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/utilities/cfcolors.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -58,35 +60,63 @@ class _TransactionFeeSelectionSheetState required int amount, required FeeRateType feeRateType, required int feeRate, + required Coin coin, }) async { switch (feeRateType) { case FeeRateType.fast: if (ref.read(feeSheetSessionCacheProvider).fast[amount] == null) { - ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount(await ref - .read(walletsChangeNotifierProvider) - .getManager(walletId) - .estimateFeeFor(amount, feeRate)); + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + + if ((coin == Coin.firo || coin == Coin.firoTestNet) && + ref.read(publicPrivateBalanceStateProvider.state).state != + "Private") { + ref.read(feeSheetSessionCacheProvider).fast[amount] = + Format.satoshisToAmount(await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate)); + } else { + ref.read(feeSheetSessionCacheProvider).fast[amount] = + Format.satoshisToAmount( + await manager.estimateFeeFor(amount, feeRate)); + } } return ref.read(feeSheetSessionCacheProvider).fast[amount]!; case FeeRateType.average: if (ref.read(feeSheetSessionCacheProvider).average[amount] == null) { - ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount(await ref - .read(walletsChangeNotifierProvider) - .getManager(walletId) - .estimateFeeFor(amount, feeRate)); + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + + if ((coin == Coin.firo || coin == Coin.firoTestNet) && + ref.read(publicPrivateBalanceStateProvider.state).state != + "Private") { + ref.read(feeSheetSessionCacheProvider).average[amount] = + Format.satoshisToAmount(await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate)); + } else { + ref.read(feeSheetSessionCacheProvider).average[amount] = + Format.satoshisToAmount( + await manager.estimateFeeFor(amount, feeRate)); + } } return ref.read(feeSheetSessionCacheProvider).average[amount]!; case FeeRateType.slow: if (ref.read(feeSheetSessionCacheProvider).slow[amount] == null) { - ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount(await ref - .read(walletsChangeNotifierProvider) - .getManager(walletId) - .estimateFeeFor(amount, feeRate)); + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + + if ((coin == Coin.firo || coin == Coin.firoTestNet) && + ref.read(publicPrivateBalanceStateProvider.state).state != + "Private") { + ref.read(feeSheetSessionCacheProvider).slow[amount] = + Format.satoshisToAmount(await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate)); + } else { + ref.read(feeSheetSessionCacheProvider).slow[amount] = + Format.satoshisToAmount( + await manager.estimateFeeFor(amount, feeRate)); + } } return ref.read(feeSheetSessionCacheProvider).slow[amount]!; } @@ -249,6 +279,7 @@ class _TransactionFeeSelectionSheetState if (feeObject != null) FutureBuilder( future: feeFor( + coin: manager.coin, feeRateType: FeeRateType.fast, feeRate: feeObject!.fast, amount: Format @@ -372,6 +403,7 @@ class _TransactionFeeSelectionSheetState if (feeObject != null) FutureBuilder( future: feeFor( + coin: manager.coin, feeRateType: FeeRateType.fast, feeRate: feeObject!.fast, amount: Format @@ -496,6 +528,7 @@ class _TransactionFeeSelectionSheetState if (feeObject != null) FutureBuilder( future: feeFor( + coin: manager.coin, feeRateType: FeeRateType.slow, feeRate: feeObject!.slow, amount: Format diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 29c0957fd..6f42ae94d 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -4483,6 +4483,70 @@ class FiroWallet extends CoinServiceAPI { return fee; } + Future estimateFeeForPublic(int satoshiAmount, int feeRate) async { + final available = + Format.decimalAmountToSatoshis(await availablePublicBalance()); + + if (available == satoshiAmount) { + return satoshiAmount - sweepAllEstimate(feeRate); + } else if (satoshiAmount <= 0 || satoshiAmount > available) { + return roughFeeEstimate(1, 2, feeRate); + } + + int runningBalance = 0; + int inputCount = 0; + for (final output in _outputsList) { + runningBalance += output.value; + inputCount++; + if (runningBalance > satoshiAmount) { + break; + } + } + + final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); + final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); + + if (runningBalance - satoshiAmount > oneOutPutFee) { + if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) { + final change = runningBalance - satoshiAmount - twoOutPutFee; + if (change > DUST_LIMIT && + runningBalance - satoshiAmount - change == twoOutPutFee) { + return runningBalance - satoshiAmount - change; + } else { + return runningBalance - satoshiAmount; + } + } else { + return runningBalance - satoshiAmount; + } + } else if (runningBalance - satoshiAmount == oneOutPutFee) { + return oneOutPutFee; + } else { + return twoOutPutFee; + } + } + + // TODO: correct formula for firo? + int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return ((181 * inputCount) + (34 * outputCount) + 10) * + (feeRatePerKB / 1000).ceil(); + } + + int sweepAllEstimate(int feeRate) { + int available = 0; + int inputCount = 0; + for (final output in _outputsList) { + if (output.status.confirmed) { + available += output.value; + inputCount++; + } + } + + // transaction will only have 1 output minus the fee + final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); + + return available - estimatedFee; + } + Future> getJMintTransactions( CachedElectrumX cachedClient, List transactions,