public firo fee estimate and firo fee ui updates

This commit is contained in:
julian 2022-09-07 10:58:54 -06:00
parent 15c4d0e617
commit 88df57177b
3 changed files with 301 additions and 98 deletions

View file

@ -167,13 +167,26 @@ class _SendViewState extends ConsumerState<SendView> {
late Future<String> _calculateFeesFuture; late Future<String> _calculateFeesFuture;
Map<int, String> cachedFees = {}; Map<int, String> cachedFees = {};
Map<int, String> cachedFiroPrivateFees = {};
Map<int, String> cachedFiroPublicFees = {};
Future<String> calculateFees(int amount) async { Future<String> calculateFees(int amount) async {
if (amount <= 0) { if (amount <= 0) {
return "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]!; return cachedFees[amount]!;
} }
@ -195,12 +208,33 @@ class _SendViewState extends ConsumerState<SendView> {
break; break;
} }
final fee = await manager.estimateFeeFor(amount, feeRate); int fee;
cachedFees[amount] = if (coin == Coin.firo || coin == Coin.firoTestNet) {
Format.satoshisToAmount(fee).toStringAsFixed(Constants.decimalPlaces); 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<String?> _firoBalanceFuture( Future<String?> _firoBalanceFuture(
@ -309,6 +343,22 @@ class _SendViewState extends ConsumerState<SendView> {
.select((value) => value.getManagerProvider(walletId))); .select((value) => value.getManagerProvider(walletId)));
final String locale = ref.watch( final String locale = ref.watch(
localeServiceChangeNotifierProvider.select((value) => value.locale)); 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( return Scaffold(
backgroundColor: CFColors.almostWhite, backgroundColor: CFColors.almostWhite,
appBar: AppBar( appBar: AppBar(
@ -1200,74 +1250,126 @@ class _SendViewState extends ConsumerState<SendView> {
Constants.size.circularBorderRadius, Constants.size.circularBorderRadius,
), ),
), ),
onPressed: () { onPressed: (coin == Coin.firo ||
showModalBottomSheet<dynamic>( coin == Coin.firoTestNet) &&
backgroundColor: Colors.transparent, ref
context: context, .watch(
shape: const RoundedRectangleBorder( publicPrivateBalanceStateProvider
borderRadius: BorderRadius.vertical( .state)
top: Radius.circular(20), .state ==
"Private"
? null
: () {
showModalBottomSheet<dynamic>(
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<SendView> {
_amountToSend!); _amountToSend!);
int availableBalance; int availableBalance;
if ((coin == Coin.firo || if ((coin == Coin.firo ||
coin == Coin.firoTestNet) coin == Coin.firoTestNet)) {
) {
if (ref if (ref
.read( .read(
publicPrivateBalanceStateProvider publicPrivateBalanceStateProvider
.state) .state)
.state == .state ==
"Private") { "Private") {
availableBalance = Format.decimalAmountToSatoshis( availableBalance =
await (manager.wallet as FiroWallet).availablePrivateBalance()); Format.decimalAmountToSatoshis(
await (manager.wallet
as FiroWallet)
.availablePrivateBalance());
} else { } else {
availableBalance = Format.decimalAmountToSatoshis( availableBalance =
await (manager.wallet as FiroWallet).availablePublicBalance()); Format.decimalAmountToSatoshis(
await (manager.wallet
as FiroWallet)
.availablePublicBalance());
} }
} else { } else {
availableBalance = availableBalance =
Format.decimalAmountToSatoshis( Format.decimalAmountToSatoshis(

View file

@ -4,6 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.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/cfcolors.dart';
import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
@ -58,35 +60,63 @@ class _TransactionFeeSelectionSheetState
required int amount, required int amount,
required FeeRateType feeRateType, required FeeRateType feeRateType,
required int feeRate, required int feeRate,
required Coin coin,
}) async { }) async {
switch (feeRateType) { switch (feeRateType) {
case FeeRateType.fast: case FeeRateType.fast:
if (ref.read(feeSheetSessionCacheProvider).fast[amount] == null) { if (ref.read(feeSheetSessionCacheProvider).fast[amount] == null) {
ref.read(feeSheetSessionCacheProvider).fast[amount] = final manager =
Format.satoshisToAmount(await ref ref.read(walletsChangeNotifierProvider).getManager(walletId);
.read(walletsChangeNotifierProvider)
.getManager(walletId) if ((coin == Coin.firo || coin == Coin.firoTestNet) &&
.estimateFeeFor(amount, feeRate)); 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]!; return ref.read(feeSheetSessionCacheProvider).fast[amount]!;
case FeeRateType.average: case FeeRateType.average:
if (ref.read(feeSheetSessionCacheProvider).average[amount] == null) { if (ref.read(feeSheetSessionCacheProvider).average[amount] == null) {
ref.read(feeSheetSessionCacheProvider).average[amount] = final manager =
Format.satoshisToAmount(await ref ref.read(walletsChangeNotifierProvider).getManager(walletId);
.read(walletsChangeNotifierProvider)
.getManager(walletId) if ((coin == Coin.firo || coin == Coin.firoTestNet) &&
.estimateFeeFor(amount, feeRate)); 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]!; return ref.read(feeSheetSessionCacheProvider).average[amount]!;
case FeeRateType.slow: case FeeRateType.slow:
if (ref.read(feeSheetSessionCacheProvider).slow[amount] == null) { if (ref.read(feeSheetSessionCacheProvider).slow[amount] == null) {
ref.read(feeSheetSessionCacheProvider).slow[amount] = final manager =
Format.satoshisToAmount(await ref ref.read(walletsChangeNotifierProvider).getManager(walletId);
.read(walletsChangeNotifierProvider)
.getManager(walletId) if ((coin == Coin.firo || coin == Coin.firoTestNet) &&
.estimateFeeFor(amount, feeRate)); 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]!; return ref.read(feeSheetSessionCacheProvider).slow[amount]!;
} }
@ -249,6 +279,7 @@ class _TransactionFeeSelectionSheetState
if (feeObject != null) if (feeObject != null)
FutureBuilder( FutureBuilder(
future: feeFor( future: feeFor(
coin: manager.coin,
feeRateType: FeeRateType.fast, feeRateType: FeeRateType.fast,
feeRate: feeObject!.fast, feeRate: feeObject!.fast,
amount: Format amount: Format
@ -372,6 +403,7 @@ class _TransactionFeeSelectionSheetState
if (feeObject != null) if (feeObject != null)
FutureBuilder( FutureBuilder(
future: feeFor( future: feeFor(
coin: manager.coin,
feeRateType: FeeRateType.fast, feeRateType: FeeRateType.fast,
feeRate: feeObject!.fast, feeRate: feeObject!.fast,
amount: Format amount: Format
@ -496,6 +528,7 @@ class _TransactionFeeSelectionSheetState
if (feeObject != null) if (feeObject != null)
FutureBuilder( FutureBuilder(
future: feeFor( future: feeFor(
coin: manager.coin,
feeRateType: FeeRateType.slow, feeRateType: FeeRateType.slow,
feeRate: feeObject!.slow, feeRate: feeObject!.slow,
amount: Format amount: Format

View file

@ -4483,6 +4483,70 @@ class FiroWallet extends CoinServiceAPI {
return fee; return fee;
} }
Future<int> 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<List<models.Transaction>> getJMintTransactions( Future<List<models.Transaction>> getJMintTransactions(
CachedElectrumX cachedClient, CachedElectrumX cachedClient,
List<String> transactions, List<String> transactions,