Skip to content

Commit 6e20f4c

Browse files
authored
Merge pull request #1759 from SatoshiPortal/price-graph-refresh
Various Minor Fixes
2 parents f579095 + c83b2a6 commit 6e20f4c

22 files changed

Lines changed: 280 additions & 90 deletions

lib/core/swaps/data/repository/boltz_swap_repository.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,10 @@ class BoltzSwapRepository {
561561
_boltz.unsubscribeToSwaps(swapIds);
562562
}
563563

564+
void subscribeToSwaps(List<String> swapIds) {
565+
_boltz.subscribeToSwaps(swapIds);
566+
}
567+
564568
Future<List<Swap>> getOngoingSwaps({String? walletId}) async {
565569
final allSwapModels = await _boltz.storage.fetchAll(isTestnet: _isTestnet);
566570

lib/core/swaps/data/services/swap_watcher.dart

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ class SwapWatcherService {
150150
if (receiveAddress == null) {
151151
throw Exception('Receive address is null');
152152
}
153+
154+
// Unsubscribe BEFORE claiming to prevent race condition with WebSocket updates
155+
_boltzRepo.unsubscribeFromSwaps([swap.id]);
156+
153157
String claimTxId;
154158
try {
155159
claimTxId = await _boltzRepo.claimLightningToBitcoinSwap(
@@ -178,8 +182,9 @@ class SwapWatcherService {
178182
fees: swap.fees?.copyWith(claimFee: swap.fees!.claimFee),
179183
);
180184
await _boltzRepo.updateSwap(swap: updatedSwap);
181-
_boltzRepo.unsubscribeFromSwaps([swap.id]);
182185
} catch (e, st) {
186+
// Re-subscribe on error so watcher continues monitoring
187+
_boltzRepo.subscribeToSwaps([swap.id]);
183188
log.severe(
184189
'{"swapId": "${swap.id}", "function": "_processReceiveLnToBitcoinClaim"}',
185190
error: e,
@@ -198,6 +203,10 @@ class SwapWatcherService {
198203
if (receiveAddress == null) {
199204
throw Exception('Receive address is null');
200205
}
206+
207+
// Unsubscribe BEFORE claiming to prevent race condition with WebSocket updates
208+
_boltzRepo.unsubscribeFromSwaps([swap.id]);
209+
201210
String claimTxId;
202211
log.fine(
203212
'{"swapId": "${swap.id}", "function": "_processReceiveLnToLiquidClaim", "action": "coop_claim_started", "timestamp": "${DateTime.now().toIso8601String()}"}',
@@ -229,9 +238,10 @@ class SwapWatcherService {
229238
fees: swap.fees?.copyWith(claimFee: swap.fees!.claimFee),
230239
);
231240
await _boltzRepo.updateSwap(swap: updatedSwap);
232-
_boltzRepo.unsubscribeFromSwaps([swap.id]);
233241
_swapStreamController.add(updatedSwap);
234242
} catch (e, st) {
243+
// Re-subscribe on error so watcher continues monitoring
244+
_boltzRepo.subscribeToSwaps([swap.id]);
235245
log.severe(
236246
'{"swapId": "${swap.id}", "function": "_processReceiveLnToLiquidClaim"}',
237247
error: e,
@@ -343,6 +353,10 @@ class SwapWatcherService {
343353
swapType: swap.type,
344354
);
345355
final absoluteFeeOptions = networkFee.toAbsolute(txSize);
356+
357+
// Unsubscribe BEFORE refunding to prevent race condition with WebSocket updates
358+
_boltzRepo.unsubscribeFromSwaps([swap.id]);
359+
346360
String refundTxid;
347361
int actualFeesUsed;
348362
log.fine(
@@ -383,8 +397,9 @@ class SwapWatcherService {
383397
fees: swap.fees?.copyWith(claimFee: actualFeesUsed),
384398
);
385399
await _boltzRepo.updateSwap(swap: updatedSwap);
386-
_boltzRepo.unsubscribeFromSwaps([swap.id]);
387400
} catch (e, st) {
401+
// Re-subscribe on error so watcher continues monitoring
402+
_boltzRepo.subscribeToSwaps([swap.id]);
388403
log.severe(
389404
'{"swapId": "${swap.id}", "function": "_processSendLiquidToLnRefund"}',
390405
error: e,
@@ -422,6 +437,10 @@ class SwapWatcherService {
422437
swapType: swap.type,
423438
);
424439
final absoluteFeeOptions = networkFee.toAbsolute(txSize);
440+
441+
// Unsubscribe BEFORE refunding to prevent race condition with WebSocket updates
442+
_boltzRepo.unsubscribeFromSwaps([swap.id]);
443+
425444
String refundTxid;
426445
int actualFeesUsed;
427446
log.fine(
@@ -462,8 +481,9 @@ class SwapWatcherService {
462481
fees: swap.fees?.copyWith(claimFee: actualFeesUsed),
463482
);
464483
await _boltzRepo.updateSwap(swap: updatedSwap);
465-
_boltzRepo.unsubscribeFromSwaps([swap.id]);
466484
} catch (e, st) {
485+
// Re-subscribe on error so watcher continues monitoring
486+
_boltzRepo.subscribeToSwaps([swap.id]);
467487
log.severe(
468488
'{"swapId": "${swap.id}", "function": "_processSendBitcoinToLnRefund"}',
469489
error: e,
@@ -506,6 +526,9 @@ class SwapWatcherService {
506526
finalClaimAddress = swap.receiveAddress!;
507527
}
508528
}
529+
// Unsubscribe BEFORE claiming to prevent race condition with WebSocket updates
530+
_boltzRepo.unsubscribeFromSwaps([swap.id]);
531+
509532
String claimTxid;
510533
log.fine(
511534
'{"swapId": "${swap.id}", "function": "_processChainLiquidToBitcoinClaim", "action": "coop_claim_started", "timestamp": "${DateTime.now().toIso8601String()}"}',
@@ -537,8 +560,9 @@ class SwapWatcherService {
537560
fees: swap.fees?.copyWith(claimFee: swap.fees!.claimFee),
538561
);
539562
await _boltzRepo.updateSwap(swap: updatedSwap);
540-
_boltzRepo.unsubscribeFromSwaps([swap.id]);
541563
} catch (e, st) {
564+
// Re-subscribe on error so watcher continues monitoring
565+
_boltzRepo.subscribeToSwaps([swap.id]);
542566
log.severe(
543567
'{"swapId": "${swap.id}", "function": "_processChainLiquidToBitcoinClaim"',
544568
error: e,
@@ -584,6 +608,9 @@ class SwapWatcherService {
584608
}
585609
}
586610

611+
// Unsubscribe BEFORE claiming to prevent race condition with WebSocket updates
612+
_boltzRepo.unsubscribeFromSwaps([swap.id]);
613+
587614
String claimTxid;
588615
log.fine(
589616
'{"swapId": "${swap.id}", "function": "_processChainBitcoinToLiquidClaim", "action": "coop_claim_started", "timestamp": "${DateTime.now().toIso8601String()}"}',
@@ -615,8 +642,9 @@ class SwapWatcherService {
615642
fees: swap.fees?.copyWith(claimFee: swap.fees!.claimFee),
616643
);
617644
await _boltzRepo.updateSwap(swap: updatedSwap);
618-
_boltzRepo.unsubscribeFromSwaps([swap.id]);
619645
} catch (e, st) {
646+
// Re-subscribe on error so watcher continues monitoring
647+
_boltzRepo.subscribeToSwaps([swap.id]);
620648
log.severe(
621649
'{"swapId": "${swap.id}", "function": "_processChainBitcoinToLiquidClaim"}',
622650
error: e,
@@ -658,6 +686,10 @@ class SwapWatcherService {
658686
refundAddressForChainSwaps: refundAddress,
659687
);
660688
final absoluteFeeOptions = networkFee.toAbsolute(txSize);
689+
690+
// Unsubscribe BEFORE refunding to prevent race condition with WebSocket updates
691+
_boltzRepo.unsubscribeFromSwaps([swap.id]);
692+
661693
String refundTxid;
662694
int actualFeesUsed;
663695
log.fine(
@@ -699,8 +731,9 @@ class SwapWatcherService {
699731
fees: swap.fees?.copyWith(claimFee: actualFeesUsed),
700732
);
701733
await _boltzRepo.updateSwap(swap: updatedSwap);
702-
_boltzRepo.unsubscribeFromSwaps([swap.id]);
703734
} catch (e, st) {
735+
// Re-subscribe on error so watcher continues monitoring
736+
_boltzRepo.subscribeToSwaps([swap.id]);
704737
log.severe(
705738
'{"swapId": "${swap.id}", "function": "_processChainLiquidToBitcoinRefund"}',
706739
error: e,
@@ -738,6 +771,10 @@ class SwapWatcherService {
738771
refundAddressForChainSwaps: refundAddress,
739772
);
740773
final absoluteFeeOptions = networkFee.toAbsolute(txSize);
774+
775+
// Unsubscribe BEFORE refunding to prevent race condition with WebSocket updates
776+
_boltzRepo.unsubscribeFromSwaps([swap.id]);
777+
741778
String refundTxid;
742779
int actualFeesUsed;
743780
log.fine(
@@ -779,8 +816,9 @@ class SwapWatcherService {
779816
fees: swap.fees?.copyWith(claimFee: actualFeesUsed),
780817
);
781818
await _boltzRepo.updateSwap(swap: updatedSwap);
782-
_boltzRepo.unsubscribeFromSwaps([swap.id]);
783819
} catch (e, st) {
820+
// Re-subscribe on error so watcher continues monitoring
821+
_boltzRepo.subscribeToSwaps([swap.id]);
784822
log.severe(
785823
'{"swapId": "${swap.id}", "function": "_processChainBitcoinToLiquidRefund"}',
786824
error: e,
@@ -810,19 +848,7 @@ class SwapWatcherService {
810848
return;
811849
case SwapType.liquidToBitcoin:
812850
case SwapType.bitcoinToLiquid:
813-
if (swap is ChainSwap &&
814-
swap.receiveTxid == null &&
815-
swap.refundTxid == null) {
816-
if (swap.status == SwapStatus.claimable) {
817-
final updatedSwap = swap.copyWith(status: SwapStatus.claimable);
818-
await _boltzRepo.updateSwap(swap: updatedSwap);
819-
} else if (swap.status == SwapStatus.refundable) {
820-
final updatedSwap = swap.copyWith(status: SwapStatus.refundable);
821-
await _boltzRepo.updateSwap(swap: updatedSwap);
822-
}
823-
} else {
824-
return;
825-
}
851+
return;
826852
}
827853
} catch (e, st) {
828854
log.severe(

lib/core/widgets/loading/status_screen.dart

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:bb_mobile/core/themes/app_theme.dart';
2+
import 'package:bb_mobile/core/utils/build_context_x.dart';
23
import 'package:bb_mobile/core/widgets/buttons/button.dart';
34
import 'package:bb_mobile/core/widgets/loading/progress_screen.dart';
45
import 'package:bb_mobile/core/widgets/template/screen_template.dart';
@@ -36,18 +37,16 @@ class StatusScreen extends StatelessWidget {
3637
return Scaffold(
3738
backgroundColor: context.appColors.onSecondary,
3839
body: StackedPage(
39-
bottomChild:
40-
(!isLoading && onTap != null)
41-
? BBButton.big(
42-
label:
43-
hasError
44-
? (buttonText ?? 'Try Again')
45-
: (buttonText ?? 'Continue'),
46-
onPressed: onTap ?? () {},
47-
textColor: context.appColors.onPrimary,
48-
bgColor: context.appColors.secondary,
49-
)
50-
: const SizedBox.shrink(),
40+
bottomChild: (!isLoading && onTap != null)
41+
? BBButton.big(
42+
label: hasError
43+
? (buttonText ?? context.loc.statusScreenTryAgain)
44+
: (buttonText ?? context.loc.statusScreenContinue),
45+
onPressed: onTap ?? () {},
46+
textColor: context.appColors.onSecondary,
47+
bgColor: context.appColors.secondary,
48+
)
49+
: const SizedBox.shrink(),
5150
child: SafeArea(
5251
child: SingleChildScrollView(
5352
child: Padding(

lib/core/widgets/template/screen_template.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:bb_mobile/core/themes/app_theme.dart';
12
import 'package:flutter/widgets.dart';
23

34
class StackedPage extends StatelessWidget {
@@ -24,6 +25,17 @@ class StackedPage extends StatelessWidget {
2425
right: 16,
2526
),
2627
alignment: Alignment.bottomCenter,
28+
decoration: BoxDecoration(
29+
gradient: LinearGradient(
30+
begin: Alignment.topCenter,
31+
end: Alignment.bottomCenter,
32+
colors: [
33+
context.appColors.onSecondary.withValues(alpha: 0.0),
34+
context.appColors.onSecondary,
35+
],
36+
stops: const [0.0, 0.3],
37+
),
38+
),
2739
child: bottomChild,
2840
),
2941
],

lib/features/bitcoin_price/presentation/bloc/bitcoin_price_bloc.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ class BitcoinPriceBloc extends Bloc<BitcoinPriceEvent, BitcoinPriceState> {
6262
try {
6363
final settings = await _getSettingsUsecase.execute();
6464
final currency = event.currency ?? settings.currencyCode;
65-
final availableCurrencies =
66-
await _getAvailableCurrenciesUsecase.execute();
65+
final availableCurrencies = await _getAvailableCurrenciesUsecase
66+
.execute();
6767

6868
final price = await _convertSatsToCurrencyAmountUsecase.execute(
6969
currencyCode: currency,
@@ -74,11 +74,13 @@ class BitcoinPriceBloc extends Bloc<BitcoinPriceEvent, BitcoinPriceState> {
7474
currency: currency,
7575
availableCurrencies: availableCurrencies,
7676
bitcoinPrice: price,
77+
startupFailed: false,
78+
error: null,
7779
),
7880
);
7981
} catch (e) {
8082
log.severe(e.toString());
81-
emit(state.copyWith(error: e));
83+
emit(state.copyWith(error: e, startupFailed: true));
8284
}
8385
}
8486

lib/features/bitcoin_price/presentation/bloc/bitcoin_price_state.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ sealed class BitcoinPriceState with _$BitcoinPriceState {
55
const factory BitcoinPriceState({
66
@Default(false) bool loadingPrice,
77
Object? error,
8+
@Default(false) bool startupFailed,
89
//
910
List<String>? availableCurrencies,
1011
String? currency,

lib/features/bitcoin_price/presentation/cubit/price_chart_cubit.dart

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ import 'package:freezed_annotation/freezed_annotation.dart';
88
part 'price_chart_cubit.freezed.dart';
99
part 'price_chart_state.dart';
1010

11+
class PriceHistoryLoadException implements Exception {
12+
const PriceHistoryLoadException();
13+
14+
@override
15+
String toString() => 'PriceHistoryLoadException';
16+
}
17+
1118
class PriceChartCubit extends Cubit<PriceChartState> {
1219
PriceChartCubit({
1320
required GetPriceHistoryUsecase getPriceHistoryUsecase,
@@ -28,9 +35,10 @@ class PriceChartCubit extends Cubit<PriceChartState> {
2835
}) async {
2936
emit(state.copyWith(isLoading: true, error: null));
3037

38+
String? selectedCurrency;
3139
try {
3240
final settings = await _getSettingsUsecase.execute();
33-
final selectedCurrency = currency ?? settings.currencyCode;
41+
selectedCurrency = currency ?? settings.currencyCode;
3442

3543
final localDayPrices = await _getPriceHistoryUsecase.execute(
3644
fromCurrency: 'BTC',
@@ -87,7 +95,7 @@ class PriceChartCubit extends Cubit<PriceChartState> {
8795

8896
refreshedAllPrices.sort((a, b) => a.createdAt.compareTo(b.createdAt));
8997

90-
if (refreshedAllPrices.isNotEmpty || localAllPrices.isEmpty) {
98+
if (refreshedAllPrices.isNotEmpty) {
9199
emit(
92100
state.copyWith(
93101
isLoading: false,
@@ -96,9 +104,24 @@ class PriceChartCubit extends Cubit<PriceChartState> {
96104
error: null,
97105
),
98106
);
107+
} else if (localAllPrices.isEmpty) {
108+
emit(
109+
state.copyWith(
110+
isLoading: false,
111+
prices: [],
112+
currency: selectedCurrency,
113+
error: const PriceHistoryLoadException(),
114+
),
115+
);
99116
}
100117
} catch (e) {
101-
emit(state.copyWith(isLoading: false, error: e));
118+
emit(
119+
state.copyWith(
120+
isLoading: false,
121+
error: e,
122+
currency: selectedCurrency ?? state.currency,
123+
),
124+
);
102125
}
103126
}
104127

lib/features/bitcoin_price/ui/price_chart_widget.dart

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,35 @@ class PriceChartWidget extends StatelessWidget {
5151
final rates = state.prices;
5252
final hasNoLocalData = rates.isEmpty;
5353

54+
if (state.error != null && hasNoLocalData) {
55+
return Center(
56+
child: Column(
57+
mainAxisAlignment: MainAxisAlignment.center,
58+
children: [
59+
BBText(
60+
context.loc.priceChartFailedToLoad,
61+
style: context.font.bodyLarge?.copyWith(
62+
color: context.appColors.onPrimary,
63+
),
64+
),
65+
const Gap(16),
66+
IconButton(
67+
onPressed: () {
68+
context.read<PriceChartCubit>().loadPriceHistory(
69+
currency: state.currency,
70+
);
71+
},
72+
icon: Icon(
73+
Icons.refresh,
74+
color: context.appColors.onPrimary,
75+
size: 32,
76+
),
77+
),
78+
],
79+
),
80+
);
81+
}
82+
5483
if (state.isLoading || hasNoLocalData) {
5584
if (state.isLoading) {
5685
return Center(

0 commit comments

Comments
 (0)