|
1 | | -import 'package:bb_mobile/core/blockchain/domain/usecases/broadcast_bitcoin_transaction_usecase.dart'; |
2 | 1 | import 'package:bb_mobile/core/fees/domain/fees_entity.dart'; |
3 | | -import 'package:bb_mobile/core/fees/domain/get_network_fees_usecase.dart'; |
| 2 | +import 'package:bb_mobile/core/utils/result.dart'; |
4 | 3 | import 'package:bb_mobile/core/wallet/domain/entities/wallet_transaction.dart'; |
5 | 4 | import 'package:bb_mobile/features/replace_by_fee/domain/bump_fee_usecase.dart'; |
6 | 5 | import 'package:bb_mobile/features/replace_by_fee/domain/fee_entity.dart'; |
7 | | -import 'package:bb_mobile/features/replace_by_fee/errors.dart'; |
| 6 | +import 'package:bb_mobile/features/replace_by_fee/domain/replace_by_fee_failure.dart'; |
8 | 7 | import 'package:bb_mobile/features/replace_by_fee/presentation/state.dart'; |
9 | | -import 'package:bdk_dart/bdk.dart' as bdk; |
10 | 8 | import 'package:flutter_bloc/flutter_bloc.dart'; |
11 | 9 |
|
12 | 10 | class ReplaceByFeeCubit extends Cubit<ReplaceByFeeState> { |
13 | 11 | final WalletTransaction originalTransaction; |
14 | 12 | final BumpFeeUsecase bumpFeeUsecase; |
15 | | - final BroadcastBitcoinTransactionUsecase broadcastBitcoinTransactionUsecase; |
16 | | - final GetNetworkFeesUsecase getNetworkFeesUsecase; |
17 | 13 |
|
18 | 14 | ReplaceByFeeCubit({ |
19 | 15 | required this.originalTransaction, |
20 | 16 | required this.bumpFeeUsecase, |
21 | | - required this.broadcastBitcoinTransactionUsecase, |
22 | | - required this.getNetworkFeesUsecase, |
23 | 17 | }) : super(const ReplaceByFeeState()) { |
24 | 18 | init(); |
25 | 19 | } |
26 | 20 |
|
27 | 21 | Future<void> init() async { |
28 | | - final fees = await getNetworkFeesUsecase.execute(isLiquid: false); |
29 | | - // Mempool fees are already RelativeFee (sat/kwu) by construction; |
30 | | - // the AbsoluteFee branch is unreachable today but kept for exhaustiveness. |
31 | | - final fastestRate = switch (fees.fastest) { |
32 | | - final RelativeFee r => r, |
33 | | - AbsoluteFee(:final sats) => NetworkFee.relativeFromAbsoluteAndVsize( |
34 | | - absoluteSats: sats, |
35 | | - vsize: originalTransaction.vsize, |
36 | | - ), |
37 | | - }; |
38 | | - final fastestFeeRate = FeeEntity( |
39 | | - type: FeeType.fastest, |
40 | | - feeRate: fastestRate, |
41 | | - ); |
42 | | - // BIP-125 requires a higher *rate* than the original to replace, with at |
43 | | - // least one extra sat/vByte. Build the recommendation in sat/vByte then |
44 | | - // narrow to RelativeFee in a single rounding step. |
45 | | - final originalSatPerVbyte = |
46 | | - originalTransaction.feeSat / originalTransaction.vsize; |
47 | | - final recommendedBumpRate = FeeEntity( |
48 | | - type: FeeType.custom, |
49 | | - feeRate: NetworkFee.relativeFromSatPerVbyte(originalSatPerVbyte + 1), |
50 | | - ); |
51 | | - emit( |
52 | | - state.copyWith( |
53 | | - fastestFeeRate: fastestFeeRate, |
54 | | - newFeeRate: recommendedBumpRate, |
55 | | - minRelay: fees.minRelay, |
56 | | - ), |
57 | | - ); |
| 22 | + switch (await bumpFeeUsecase.getNetworkFees()) { |
| 23 | + case Ok(:final value): |
| 24 | + // Mempool fees are already RelativeFee (sat/kwu) by construction; |
| 25 | + // the AbsoluteFee branch is unreachable today but kept for exhaustiveness. |
| 26 | + final fastestRate = switch (value.fastest) { |
| 27 | + final RelativeFee r => r, |
| 28 | + AbsoluteFee(:final sats) => NetworkFee.relativeFromAbsoluteAndVsize( |
| 29 | + absoluteSats: sats, |
| 30 | + vsize: originalTransaction.vsize, |
| 31 | + ), |
| 32 | + }; |
| 33 | + final originalSatPerVbyte = |
| 34 | + originalTransaction.feeSat / originalTransaction.vsize; |
| 35 | + emit( |
| 36 | + state.copyWith( |
| 37 | + fastestFeeRate: FeeEntity(type: FeeType.fastest, feeRate: fastestRate), |
| 38 | + newFeeRate: FeeEntity( |
| 39 | + type: FeeType.custom, |
| 40 | + feeRate: NetworkFee.relativeFromSatPerVbyte(originalSatPerVbyte + 1), |
| 41 | + ), |
| 42 | + minRelay: value.minRelay, |
| 43 | + ), |
| 44 | + ); |
| 45 | + case Err(:final failure): |
| 46 | + emit(state.copyWith(failure: failure)); |
| 47 | + } |
58 | 48 | } |
59 | 49 |
|
60 | | - void clearError() => emit(state.copyWith(error: null)); |
61 | | - |
62 | | - void reset() => emit(const ReplaceByFeeState()); |
63 | | - |
64 | 50 | Future<void> broadcast() async { |
65 | | - try { |
66 | | - emit(state.copyWith(error: null)); |
67 | | - |
68 | | - if (state.newFeeRate == null) { |
69 | | - emit(state.copyWith(error: NoFeeRateSelectedError())); |
70 | | - return; |
71 | | - } |
| 51 | + emit(state.copyWith(failure: null)); |
72 | 52 |
|
73 | | - // The custom field currently shows a below-floor/empty rate. newFeeRate |
74 | | - // still holds the last valid value, so without this guard Broadcast |
75 | | - // would fire the stale rate the user no longer sees. |
76 | | - if (state.customFeeBelowFloor) { |
77 | | - emit(state.copyWith(error: FeeRateTooLowError())); |
78 | | - return; |
79 | | - } |
80 | | - |
81 | | - final psbt = await bumpFeeUsecase.execute( |
82 | | - walletId: originalTransaction.walletId, |
83 | | - txid: originalTransaction.txId, |
84 | | - newFeeRate: state.newFeeRate!.feeRate, |
85 | | - ); |
| 53 | + if (state.newFeeRate == null) { |
| 54 | + emit(state.copyWith(failure: const ReplaceByFeeNoFeeRateSelectedFailure())); |
| 55 | + return; |
| 56 | + } |
86 | 57 |
|
87 | | - final txid = await broadcastBitcoinTransactionUsecase.execute( |
88 | | - psbt, |
89 | | - isPsbt: true, |
90 | | - ); |
| 58 | + // The custom field currently shows a below-floor/empty rate. newFeeRate |
| 59 | + // still holds the last valid value, so without this guard broadcast |
| 60 | + // would fire the stale rate the user no longer sees. |
| 61 | + if (state.customFeeBelowFloor) { |
| 62 | + emit(state.copyWith(failure: const ReplaceByFeeFeeRateTooLowFailure())); |
| 63 | + return; |
| 64 | + } |
91 | 65 |
|
92 | | - emit(state.copyWith(txid: txid)); |
93 | | - // Just replacing with a similar bdk_dart error here for migration from bdk_flutter, |
94 | | - // but no library errors should leak into the presentation layer, |
95 | | - // we should catch them in the usecase and throw application errors instead |
96 | | - } on bdk.FeeRateTooLowCreateTxException catch (_) { |
97 | | - emit(state.copyWith(error: FeeRateTooLowError())); |
98 | | - } catch (e) { |
99 | | - emit(state.copyWith(error: GenericError())); |
| 66 | + switch (await bumpFeeUsecase.execute( |
| 67 | + walletId: originalTransaction.walletId, |
| 68 | + txid: originalTransaction.txId, |
| 69 | + newFeeRate: state.newFeeRate!.feeRate, |
| 70 | + )) { |
| 71 | + case Ok(:final value): |
| 72 | + emit(state.copyWith(txid: value)); |
| 73 | + case Err(:final failure): |
| 74 | + emit(state.copyWith(failure: failure)); |
100 | 75 | } |
101 | 76 | } |
102 | 77 |
|
103 | 78 | /// A valid (above-floor) selection — from a custom keystroke or the Fastest |
104 | | - /// tile. Clears any prior below-floor flag. |
| 79 | + /// tile. Clears any prior below-floor flag and any broadcast failure. |
105 | 80 | void onChangeFee(FeeEntity fee) => |
106 | | - emit(state.copyWith(newFeeRate: fee, customFeeBelowFloor: false)); |
| 81 | + emit(state.copyWith(newFeeRate: fee, customFeeBelowFloor: false, failure: null)); |
107 | 82 |
|
108 | 83 | /// The custom field went below the relay floor or was emptied. Keep |
109 | 84 | /// [newFeeRate] (the last valid value / init sentinel) but flag the field so |
|
0 commit comments