Skip to content

Commit 0c13fd4

Browse files
fix(sell): stop stray polling, restore broadcast logs, drop dead fee
1 parent e96397c commit 0c13fd4

12 files changed

Lines changed: 72 additions & 86 deletions

lib/features/sell/domain/usecases/confirm_sell_payin_usecase.dart

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import 'package:bb_mobile/core/utils/logger.dart';
99
import 'package:bb_mobile/core/utils/result.dart';
1010
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
1111
import 'package:bb_mobile/core/wallet/domain/entities/wallet_utxo.dart';
12-
import 'package:bb_mobile/core/wallet/domain/usecases/calculate_bitcoin_absolute_fees_usecase.dart';
1312
import 'package:bb_mobile/core/wallet/domain/usecases/prepare_bitcoin_send_usecase.dart';
1413
import 'package:bb_mobile/features/labels/labels_facade.dart';
1514
import 'package:bb_mobile/features/sell/domain/sell_failure.dart';
@@ -33,21 +32,20 @@ Future<String> _defaultLiquidTxidFromPset(String pset) async =>
3332

3433
/// Builds, signs and broadcasts the payin transaction for a sell order.
3534
///
36-
/// The broadcast is the point of no return: once it succeeds the user's money
37-
/// has moved, so the payin is reported as a success even if the after-the-fact
35+
/// The broadcast is the point of no return: once it succeeds the money has
36+
/// moved, so the payin is reported as a success even if the after-the-fact
3837
/// bookkeeping (transaction labelling) fails — that is logged but never turned
3938
/// into a [SellSendPaymentFailure], which would otherwise invite a double-spend
4039
/// retry. Every failure before or during broadcast is mapped to a sealed
41-
/// [SellFailure], and the raw exception never leaves the boundary.
40+
/// [SellFailure]; the raw exception is logged for diagnosis but never carried
41+
/// in the failure value that reaches bloc state.
4242
class ConfirmSellPayinUsecase {
4343
final PrepareBitcoinSendUsecase _prepareBitcoinSendUsecase;
4444
final PrepareLiquidSendUsecase _prepareLiquidSendUsecase;
4545
final SignBitcoinTxUsecase _signBitcoinTxUsecase;
4646
final SignLiquidTxUsecase _signLiquidTxUsecase;
4747
final BroadcastBitcoinTransactionUsecase _broadcastBitcoinTransactionUsecase;
4848
final BroadcastLiquidTransactionUsecase _broadcastLiquidTransactionUsecase;
49-
final CalculateBitcoinAbsoluteFeesUsecase
50-
_calculateBitcoinAbsoluteFeesUsecase;
5149
final LabelsFacade _labelsFacade;
5250
final BitcoinTxidFromPsbt _bitcoinTxidFromPsbt;
5351
final LiquidTxidFromPset _liquidTxidFromPset;
@@ -59,15 +57,13 @@ class ConfirmSellPayinUsecase {
5957
required this._signLiquidTxUsecase,
6058
required this._broadcastBitcoinTransactionUsecase,
6159
required this._broadcastLiquidTransactionUsecase,
62-
required this._calculateBitcoinAbsoluteFeesUsecase,
6360
required this._labelsFacade,
6461
this._bitcoinTxidFromPsbt = _defaultBitcoinTxidFromPsbt,
6562
this._liquidTxidFromPset = _defaultLiquidTxidFromPset,
6663
});
6764

6865
@useResult
69-
Future<Result<({String txid, int? updatedAbsoluteFees}), SellFailure>>
70-
execute({
66+
Future<Result<String, SellFailure>> execute({
7167
required Wallet wallet,
7268
required SellOrder sellOrder,
7369
required int? absoluteFees,
@@ -77,7 +73,6 @@ class ConfirmSellPayinUsecase {
7773
final payinAmountSat = ConvertAmount.btcToSats(sellOrder.payinAmount);
7874

7975
final String txid;
80-
final int? updatedAbsoluteFees;
8176
try {
8277
if (wallet.isLiquid) {
8378
final pset = await _prepareLiquidSendUsecase.execute(
@@ -94,7 +89,6 @@ class ConfirmSellPayinUsecase {
9489
// Derive the txid before broadcasting so nothing fallible runs after
9590
// the money has moved.
9691
txid = await _liquidTxidFromPset(signedPset);
97-
updatedAbsoluteFees = null;
9892
await _broadcastLiquidTransactionUsecase.execute(signedPset);
9993
} else {
10094
if (absoluteFees == null) {
@@ -108,8 +102,6 @@ class ConfirmSellPayinUsecase {
108102
selectedInputs: selectedInputs.isNotEmpty ? selectedInputs : null,
109103
replaceByFee: replaceByFee,
110104
);
111-
updatedAbsoluteFees = await _calculateBitcoinAbsoluteFeesUsecase
112-
.execute(psbt: preparedSend.unsignedPsbt);
113105
final signedTx = await _signBitcoinTxUsecase.execute(
114106
psbt: preparedSend.unsignedPsbt,
115107
walletId: wallet.id,
@@ -121,19 +113,15 @@ class ConfirmSellPayinUsecase {
121113
);
122114
}
123115
} catch (e, st) {
124-
log.severe(
125-
message: 'sell confirm payin failed',
126-
error: e.runtimeType,
127-
trace: st,
128-
);
116+
log.severe(message: 'sell confirm payin failed', error: e, trace: st);
129117
return Err(SellSendPaymentFailure(e.runtimeType.toString()));
130118
}
131119

132120
// Broadcast succeeded: the payin is done. Labelling is best-effort and must
133121
// never demote a completed payin to a failure.
134122
await _storeSellLabel(walletId: wallet.id, txid: txid);
135123

136-
return Ok((txid: txid, updatedAbsoluteFees: updatedAbsoluteFees));
124+
return Ok(txid);
137125
}
138126

139127
Future<void> _storeSellLabel({
@@ -151,7 +139,7 @@ class ConfirmSellPayinUsecase {
151139
} catch (e, st) {
152140
log.warning(
153141
'sell payin label store failed (payin already broadcast)',
154-
error: e.runtimeType,
142+
error: e,
155143
trace: st,
156144
);
157145
}

lib/features/sell/domain/usecases/estimate_sell_payin_fees_usecase.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import 'package:meta/meta.dart';
1818
/// order is created: fetches the exchange rate, derives the required amount,
1919
/// enforces the sufficient-balance rule, and computes the absolute network fee
2020
/// by building a throwaway transaction to a wallet-owned address.
21-
2221
class EstimateSellPayinFeesUsecase {
2322
final ConvertSatsToCurrencyAmountUsecase _convertSatsToCurrencyAmountUsecase;
2423
final GetAddressAtIndexUsecase _getAddressAtIndexUsecase;

lib/features/sell/domain/usecases/get_sell_order_status_usecase.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import 'package:bb_mobile/features/sell/domain/sell_failure.dart';
66
import 'package:meta/meta.dart';
77

88
/// Fetches the latest state of a sell order while polling.
9-
109
class GetSellOrderStatusUsecase {
1110
final GetOrderUsecase _getOrderUsecase;
1211

lib/features/sell/domain/usecases/recalculate_sell_payin_fees_usecase.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import 'package:meta/meta.dart';
1515
/// Recomputes the absolute network fee of an already-created sell payin when the
1616
/// user changes coin selection or RBF, by building a throwaway transaction to a
1717
/// wallet-owned address.
18-
1918
class RecalculateSellPayinFeesUsecase {
2019
final GetAddressAtIndexUsecase _getAddressAtIndexUsecase;
2120
final GetNetworkFeesUsecase _getNetworkFeesUsecase;

lib/features/sell/domain/usecases/start_sell_usecase.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import 'package:meta/meta.dart';
99

1010
/// Loads the data needed to open the sell amount-input screen: the exchange user
1111
/// summary and the user's bitcoin unit.
12-
1312
class StartSellUsecase {
1413
final GetExchangeUserSummaryUsecase _getExchangeUserSummaryUsecase;
1514
final GetSettingsUsecase _getSettingsUsecase;

lib/features/sell/presentation/bloc/sell_bloc.dart

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -254,9 +254,11 @@ class SellBloc extends Bloc<SellEvent, SellState> {
254254
orderId: paymentState.sellOrder.orderId,
255255
)) {
256256
case Ok(:final value):
257-
emit(paymentState.copyWith(sellOrder: value));
258-
case Err(:final failure):
259-
emit(paymentState.copyWith(error: failure));
257+
emit(paymentState.copyWith(sellOrder: value, error: null));
258+
case Err():
259+
// Background refresh: the use-case already logged it. Don't surface a
260+
// transient failure; the next refresh heals it.
261+
return;
260262
}
261263
}
262264

@@ -276,9 +278,15 @@ class SellBloc extends Bloc<SellEvent, SellState> {
276278

277279
final wallet = sellPaymentState.selectedWallet;
278280
if (wallet == null) {
281+
log.severe(
282+
error: 'no wallet selected to send payment',
283+
trace: StackTrace.current,
284+
);
279285
emit(
280286
sellPaymentState.copyWith(
281-
error: const SellUnexpectedFailure(),
287+
error: const SellUnexpectedFailure(
288+
'no wallet selected to send payment',
289+
),
282290
isConfirmingPayment: false,
283291
),
284292
);
@@ -299,13 +307,17 @@ class SellBloc extends Bloc<SellEvent, SellState> {
299307
sellPaymentState.copyWith(error: failure, isConfirmingPayment: false),
300308
);
301309
return;
302-
case Ok(:final value):
303-
final updatedState = value.updatedAbsoluteFees != null
304-
? sellPaymentState.copyWith(absoluteFees: value.updatedAbsoluteFees)
305-
: sellPaymentState;
310+
case Ok():
311+
// The payin is broadcast; the payment flow is done. Stop the status
312+
// poller so it doesn't keep firing no-op events on the success state.
313+
_stopPolling();
306314
// 5s delay gives backend time to register the 0 conf
307315
await Future.delayed(const Duration(seconds: 5));
308-
emit(updatedState.toSuccessState(sellOrder: updatedState.sellOrder));
316+
emit(
317+
sellPaymentState.toSuccessState(
318+
sellOrder: sellPaymentState.sellOrder,
319+
),
320+
);
309321
}
310322
}
311323

@@ -323,8 +335,9 @@ class SellBloc extends Bloc<SellEvent, SellState> {
323335
)) {
324336
case Ok(:final value):
325337
latestOrder = value;
326-
case Err(:final failure):
327-
emit(sellPaymentState.copyWith(error: failure));
338+
case Err():
339+
// Background retry: the use-case already logged it. Don't paint a
340+
// persistent error on a transient blip — the next poll heals it.
328341
return;
329342
}
330343

@@ -336,11 +349,17 @@ class SellBloc extends Bloc<SellEvent, SellState> {
336349
_stopPolling();
337350
emit(
338351
sellPaymentState
339-
.copyWith(sellOrder: latestOrder, isPolling: false)
352+
.copyWith(sellOrder: latestOrder, isPolling: false, error: null)
340353
.toSuccessState(sellOrder: latestOrder),
341354
);
342355
} else {
343-
emit(sellPaymentState.copyWith(sellOrder: latestOrder, isPolling: true));
356+
emit(
357+
sellPaymentState.copyWith(
358+
sellOrder: latestOrder,
359+
isPolling: true,
360+
error: null,
361+
),
362+
);
344363
}
345364
}
346365

lib/features/sell/sell_locator.dart

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,6 @@ class SellLocator {
106106
locator<BroadcastBitcoinTransactionUsecase>(),
107107
broadcastLiquidTransactionUsecase:
108108
locator<BroadcastLiquidTransactionUsecase>(),
109-
calculateBitcoinAbsoluteFeesUsecase:
110-
locator<CalculateBitcoinAbsoluteFeesUsecase>(),
111109
labelsFacade: locator<LabelsFacade>(),
112110
),
113111
);

localization/app_hy.arb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,9 +1028,9 @@
10281028
"sellWhichWalletQuestion": "Ո՞ր դրամապանակից եք ցանկանում վաճառել",
10291029
"sellExternalWallet": "Արտաքին դրամապանակ",
10301030
"sellFromAnotherWallet": "Վաճառել այլ Bitcoin դրամապանակից",
1031-
"sellAboveMaxAmountError": "Դուք փորձում եք վաճառել այս դրամապանակով թույլատրվող առավելագույն գումարից ավելի։",
1032-
"sellBelowMinAmountError": "Դուք փորձում եք վաճառել այս դրամապանակով թույլատրվող նվազագույն գումարից պակաս։",
1033-
"sellInsufficientBalanceError": "Ընտրված դրամապանակում բավարար մնացորդ չկա այս վաճառքի պատվերն ավարտելու համար։",
1031+
"sellAboveMaxAmountError": "Այս դրամապանակով վաճառքի առավելագույն գումարն է {amount}։",
1032+
"sellBelowMinAmountError": "Այս դրամապանակով վաճառքի նվազագույն գումարն է {amount}։",
1033+
"sellInsufficientBalanceError": "Ընտրված դրամապանակում բավարար մնացորդ չկա այս վաճառքի պատվերն ավարտելու համար։ Պահանջվում է՝ {amount}։",
10341034
"sellUnauthenticatedError": "Դուք նույնականացված չեք։ Խնդրում ենք մուտք գործել՝ շարունակելու համար։",
10351035
"sellOrderNotFoundError": "Վաճառքի պատվերը չի գտնվել։ Խնդրում ենք կրկին փորձել։",
10361036
"sellOrderAlreadyConfirmedError": "Այս վաճառքի պատվերն արդեն հաստատված է։",

localization/app_ka.arb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,9 +1228,9 @@
12281228
"sellWhichWalletQuestion": "რომელი საფულიდან გსურთ გაყიდვა?",
12291229
"sellExternalWallet": "გარე საფულე",
12301230
"sellFromAnotherWallet": "გაყიდეთ სხვა Bitcoin საფულედან",
1231-
"sellAboveMaxAmountError": "თქვენ ცდილობთ გაყიდოთ იმაზე მეტი, ვიდრე ამ საფულით ნებადართული მაქსიმალური თანხაა.",
1232-
"sellBelowMinAmountError": "თქვენ ცდილობთ გაყიდოთ იმაზე ნაკლები, ვიდრე ამ საფულით ნებადართული მინიმალური თანხაა.",
1233-
"sellInsufficientBalanceError": "არჩეულ საფულეში არასაკმარისი ბალანსია ამ გაყიდვის შეკვეთის დასასრულებლად.",
1231+
"sellAboveMaxAmountError": "ამ საფულით გასაყიდი მაქსიმალური თანხაა {amount}.",
1232+
"sellBelowMinAmountError": "ამ საფულით გასაყიდი მინიმალური თანხაა {amount}.",
1233+
"sellInsufficientBalanceError": "არჩეულ საფულეში არასაკმარისი ბალანსია ამ გაყიდვის შეკვეთის დასასრულებლად. საჭიროა: {amount}.",
12341234
"sellUnauthenticatedError": "თქვენ არ ხართ ავთენტიფიცირებული. გასაგრძელებლად გთხოვთ შეხვიდეთ სისტემაში.",
12351235
"sellOrderNotFoundError": "გაყიდვის შეკვეთა ვერ მოიძებნა. გთხოვთ, სცადოთ ხელახლა.",
12361236
"sellOrderAlreadyConfirmedError": "ეს გაყიდვის შეკვეთა უკვე დადასტურებულია.",

localization/app_sw.arb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,9 +1045,9 @@
10451045
"sellWhichWalletQuestion": "Ni pochi gani unayotaka kuuza kutoka?",
10461046
"sellExternalWallet": "Pochi ya nje",
10471047
"sellFromAnotherWallet": "Uza kutoka pochi nyingine ya Bitcoin",
1048-
"sellAboveMaxAmountError": "Unajaribu kuuza zaidi ya kiwango cha juu kinachoweza kuuzwa kwa pochi hii.",
1049-
"sellBelowMinAmountError": "Unajaribu kuuza chini ya kiwango cha chini kinachoweza kuuzwa kwa pochi hii.",
1050-
"sellInsufficientBalanceError": "Salio halitoshi kwenye pochi iliyochaguliwa kukamilisha agizo hili la kuuza.",
1048+
"sellAboveMaxAmountError": "Kiasi cha juu kinachoweza kuuzwa kwa pochi hii ni {amount}.",
1049+
"sellBelowMinAmountError": "Kiasi cha chini kinachoweza kuuzwa kwa pochi hii ni {amount}.",
1050+
"sellInsufficientBalanceError": "Salio halitoshi kwenye pochi iliyochaguliwa kukamilisha agizo hili la kuuza. Kinachohitajika: {amount}.",
10511051
"sellUnauthenticatedError": "Hujathibitishwa. Tafadhali ingia ili kuendelea.",
10521052
"sellOrderNotFoundError": "Agizo la kuuza halikupatikana. Tafadhali jaribu tena.",
10531053
"sellOrderAlreadyConfirmedError": "Agizo hili la kuuza tayari limethibitishwa.",

0 commit comments

Comments
 (0)