Skip to content

Commit 977222d

Browse files
committed
feat(spl): fee selection
TODO dynamic estimation via api
1 parent 61e0102 commit 977222d

File tree

3 files changed

+169
-44
lines changed

3 files changed

+169
-44
lines changed

lib/pages/send_view/sol_token_send_view.dart

Lines changed: 132 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import 'package:decimal/decimal.dart';
1414
import 'package:flutter/material.dart';
1515
import 'package:flutter/services.dart';
1616
import 'package:flutter_riverpod/flutter_riverpod.dart';
17+
import 'package:flutter_svg/flutter_svg.dart';
1718

1819
import '../../models/isar/models/isar_models.dart';
1920
import '../../models/send_view_auto_fill_data.dart';
@@ -28,7 +29,9 @@ import '../../utilities/amount/amount_formatter.dart';
2829
import '../../utilities/amount/amount_input_formatter.dart';
2930
import '../../utilities/barcode_scanner_interface.dart';
3031
import '../../utilities/clipboard_interface.dart';
32+
import '../../utilities/assets.dart';
3133
import '../../utilities/constants.dart';
34+
import '../../utilities/enums/fee_rate_type_enum.dart';
3235
import '../../utilities/logger.dart';
3336
import '../../utilities/prefs.dart';
3437
import '../../utilities/text_styles.dart';
@@ -340,10 +343,45 @@ class _SolTokenSendViewState extends ConsumerState<SolTokenSendView> {
340343
}
341344

342345
Future<String> calculateFees() async {
343-
// TODO: Implement Solana fee calculation.
344-
// For now, return a placeholder fee
345-
cachedFees = "0.000005 SOL";
346-
return cachedFees;
346+
try {
347+
final wallet = ref.read(pCurrentSolanaTokenWallet);
348+
if (wallet == null) {
349+
return "0.000005 SOL";
350+
}
351+
352+
final feeObject = await wallet.fees;
353+
354+
late final BigInt feeRate;
355+
356+
switch (ref.read(feeRateTypeMobileStateProvider.state).state) {
357+
case FeeRateType.fast:
358+
feeRate = feeObject.fast;
359+
break;
360+
case FeeRateType.average:
361+
feeRate = feeObject.medium;
362+
break;
363+
case FeeRateType.slow:
364+
feeRate = feeObject.slow;
365+
break;
366+
default:
367+
feeRate = BigInt.from(-1);
368+
}
369+
370+
final Amount fee = await wallet.estimateFeeFor(Amount.zero, feeRate);
371+
cachedFees = ref
372+
.read(pAmountFormatter(Solana(CryptoCurrencyNetwork.main)))
373+
.format(fee, withUnitName: true, indicatePrecisionLoss: false);
374+
375+
return cachedFees;
376+
} catch (e, s) {
377+
Logging.instance.w(
378+
"Failed to calculate Solana token fees: ",
379+
error: e,
380+
stackTrace: s,
381+
);
382+
// Return minimum fee as fallback.
383+
return "0.000005 SOL";
384+
}
347385
}
348386

349387
Future<void> _previewTransaction() async {
@@ -522,6 +560,7 @@ class _SolTokenSendViewState extends ConsumerState<SolTokenSendView> {
522560
@override
523561
void initState() {
524562
ref.refresh(feeSheetSessionCacheProvider);
563+
ref.read(feeRateTypeMobileStateProvider.state).state = FeeRateType.slow;
525564

526565
_calculateFeesFuture = calculateFees();
527566
_data = widget.autoFillData;
@@ -1114,38 +1153,100 @@ class _SolTokenSendViewState extends ConsumerState<SolTokenSendView> {
11141153
),
11151154
),
11161155
onPressed: () {
1117-
// TODO: Implement fee selection for Solana.
1156+
showModalBottomSheet<dynamic>(
1157+
backgroundColor: Colors.transparent,
1158+
context: context,
1159+
shape: const RoundedRectangleBorder(
1160+
borderRadius: BorderRadius.vertical(
1161+
top: Radius.circular(20),
1162+
),
1163+
),
1164+
builder: (_) =>
1165+
TransactionFeeSelectionSheet(
1166+
walletId: walletId,
1167+
isToken: true,
1168+
amount:
1169+
(Decimal.tryParse(
1170+
cryptoAmountController
1171+
.text,
1172+
) ??
1173+
Decimal.zero)
1174+
.toAmount(
1175+
fractionDigits:
1176+
tokenWallet
1177+
.tokenDecimals,
1178+
),
1179+
updateChosen: (String fee) {
1180+
setState(() {
1181+
_calculateFeesFuture = Future(
1182+
() => fee,
1183+
);
1184+
});
1185+
},
1186+
),
1187+
);
11181188
},
11191189
child: Row(
11201190
mainAxisAlignment:
11211191
MainAxisAlignment.spaceBetween,
11221192
children: [
1123-
FutureBuilder(
1124-
future: _calculateFeesFuture,
1125-
builder: (context, snapshot) {
1126-
if (snapshot.connectionState ==
1127-
ConnectionState.done &&
1128-
snapshot.hasData) {
1129-
return Text(
1130-
"~${snapshot.data!}",
1131-
style: STextStyles.itemSubtitle(
1132-
context,
1133-
),
1134-
);
1135-
} else {
1136-
return AnimatedText(
1137-
stringsToLoopThrough: const [
1138-
"Calculating",
1139-
"Calculating.",
1140-
"Calculating..",
1141-
"Calculating...",
1142-
],
1143-
style: STextStyles.itemSubtitle(
1144-
context,
1145-
),
1146-
);
1147-
}
1148-
},
1193+
Row(
1194+
children: [
1195+
Text(
1196+
ref
1197+
.watch(
1198+
feeRateTypeMobileStateProvider
1199+
.state,
1200+
)
1201+
.state
1202+
.prettyName,
1203+
style: STextStyles.itemSubtitle12(
1204+
context,
1205+
),
1206+
),
1207+
const SizedBox(width: 10),
1208+
FutureBuilder(
1209+
future: _calculateFeesFuture,
1210+
builder: (context, snapshot) {
1211+
if (snapshot.connectionState ==
1212+
ConnectionState.done &&
1213+
snapshot.hasData) {
1214+
return Text(
1215+
"~${snapshot.data!}",
1216+
style:
1217+
STextStyles.itemSubtitle(
1218+
context,
1219+
),
1220+
);
1221+
} else {
1222+
return AnimatedText(
1223+
stringsToLoopThrough:
1224+
const [
1225+
"Calculating",
1226+
"Calculating.",
1227+
"Calculating..",
1228+
"Calculating...",
1229+
],
1230+
style:
1231+
STextStyles.itemSubtitle(
1232+
context,
1233+
),
1234+
);
1235+
}
1236+
},
1237+
),
1238+
],
1239+
),
1240+
SvgPicture.asset(
1241+
Assets.svg.chevronDown,
1242+
width: 8,
1243+
height: 4,
1244+
colorFilter: ColorFilter.mode(
1245+
Theme.of(context)
1246+
.extension<StackColors>()!
1247+
.textSubtitle2,
1248+
BlendMode.srcIn,
1249+
),
11491250
),
11501251
],
11511252
),

lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class _DesktopSendFeeFormState extends ConsumerState<DesktopSendFeeForm> {
7777
Widget build(BuildContext context) {
7878
final canEditFees =
7979
isEth ||
80+
cryptoCurrency is Solana ||
8081
(cryptoCurrency is ElectrumXCurrencyInterface &&
8182
!(((cryptoCurrency is Firo) &&
8283
(ref.watch(publicPrivateBalanceStateProvider.state).state ==
@@ -211,7 +212,7 @@ class _DesktopSendFeeFormState extends ConsumerState<DesktopSendFeeForm> {
211212
.estimateFeeFor(amount, feeRate);
212213
}
213214
} else {
214-
// TODO: Implement fee estimation for Solana tokens.
215+
// Token fee estimation (works for ERC20 and SPL tokens).
215216
try {
216217
final tokenWallet = ref.read(
217218
pCurrentTokenWallet,
@@ -223,7 +224,7 @@ class _DesktopSendFeeFormState extends ConsumerState<DesktopSendFeeForm> {
223224
.average[amount] =
224225
fee;
225226
} catch (_) {
226-
// Token wallet not available (Solana).
227+
// Token wallet not available.
227228
debugPrint("Token fee estimation not available");
228229
}
229230
}

lib/wallets/wallet/impl/solana_wallet.dart

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -289,35 +289,58 @@ class SolanaWallet extends Bip39Wallet<Solana> {
289289
);
290290
}
291291

292-
final fee = await _getEstimatedNetworkFee(amount);
293-
if (fee == null) {
294-
throw Exception("Failed to get fees, please check your node connection.");
295-
}
296-
297-
return Amount(rawValue: fee, fractionDigits: cryptoCurrency.fractionDigits);
292+
// The feeRate parameter contains the total fee amount to use.
293+
// For Solana, this is already calculated based on priority tier.
294+
// Simply return it as the fee estimate.
295+
return Amount(rawValue: feeRate, fractionDigits: cryptoCurrency.fractionDigits);
298296
}
299297

300298
@override
301299
Future<FeeObject> get fees async {
302300
_checkClient();
303301

304-
final fee = await _getEstimatedNetworkFee(
302+
final baseFee = await _getEstimatedNetworkFee(
305303
Amount.fromDecimal(
306304
Decimal.one, // 1 SOL.
307305
fractionDigits: cryptoCurrency.fractionDigits,
308306
),
309307
);
310-
if (fee == null) {
308+
if (baseFee == null) {
311309
throw Exception("Failed to get fees, please check your node connection.");
312310
}
313311

312+
// Differentiate fees by tier using multipliers:
313+
// Base fee is typically around 5000 lamports.
314+
// Slow: minimum 5000 lamports.
315+
// Average: base fee * 1.5 (but not less than slow).
316+
// Fast: base fee * 2.0 (but not less than average).
317+
// Ensure all fees stay within bounds: 5000-1000000 lamports.
318+
const minFeeBig = 5000;
319+
const maxFeeBig = 1000000;
320+
321+
// Calculate tier fees with multipliers.
322+
final slowFee = baseFee; // Use base fee for slow.
323+
final averageFee = (baseFee * BigInt.from(3)) ~/ BigInt.from(2); // 1.5x.
324+
final fastFee = baseFee * BigInt.from(2); // 2.0x.
325+
326+
// Clamp all fees to the allowed range.
327+
final _clamp = (BigInt value) {
328+
if (value < BigInt.from(minFeeBig)) return BigInt.from(minFeeBig);
329+
if (value > BigInt.from(maxFeeBig)) return BigInt.from(maxFeeBig);
330+
return value;
331+
};
332+
333+
final clampedSlow = _clamp(slowFee);
334+
final clampedAverage = _clamp(averageFee);
335+
final clampedFast = _clamp(fastFee);
336+
314337
return FeeObject(
315338
numberOfBlocksFast: 1,
316339
numberOfBlocksAverage: 1,
317340
numberOfBlocksSlow: 1,
318-
fast: fee,
319-
medium: fee,
320-
slow: fee,
341+
fast: clampedFast,
342+
medium: clampedAverage,
343+
slow: clampedSlow,
321344
);
322345
}
323346

0 commit comments

Comments
 (0)