Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/core/swaps/data/repository/boltz_swap_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,10 @@ class BoltzSwapRepository {
_boltz.unsubscribeToSwaps(swapIds);
}

void subscribeToSwaps(List<String> swapIds) {
_boltz.subscribeToSwaps(swapIds);
}

Future<List<Swap>> getOngoingSwaps({String? walletId}) async {
final allSwapModels = await _boltz.storage.fetchAll(isTestnet: _isTestnet);

Expand Down
68 changes: 47 additions & 21 deletions lib/core/swaps/data/services/swap_watcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ class SwapWatcherService {
if (receiveAddress == null) {
throw Exception('Receive address is null');
}

// Unsubscribe BEFORE claiming to prevent race condition with WebSocket updates
_boltzRepo.unsubscribeFromSwaps([swap.id]);

String claimTxId;
try {
claimTxId = await _boltzRepo.claimLightningToBitcoinSwap(
Expand Down Expand Up @@ -178,8 +182,9 @@ class SwapWatcherService {
fees: swap.fees?.copyWith(claimFee: swap.fees!.claimFee),
);
await _boltzRepo.updateSwap(swap: updatedSwap);
_boltzRepo.unsubscribeFromSwaps([swap.id]);
} catch (e, st) {
// Re-subscribe on error so watcher continues monitoring
_boltzRepo.subscribeToSwaps([swap.id]);
log.severe(
'{"swapId": "${swap.id}", "function": "_processReceiveLnToBitcoinClaim"}',
error: e,
Expand All @@ -198,6 +203,10 @@ class SwapWatcherService {
if (receiveAddress == null) {
throw Exception('Receive address is null');
}

// Unsubscribe BEFORE claiming to prevent race condition with WebSocket updates
_boltzRepo.unsubscribeFromSwaps([swap.id]);

String claimTxId;
log.fine(
'{"swapId": "${swap.id}", "function": "_processReceiveLnToLiquidClaim", "action": "coop_claim_started", "timestamp": "${DateTime.now().toIso8601String()}"}',
Expand Down Expand Up @@ -229,9 +238,10 @@ class SwapWatcherService {
fees: swap.fees?.copyWith(claimFee: swap.fees!.claimFee),
);
await _boltzRepo.updateSwap(swap: updatedSwap);
_boltzRepo.unsubscribeFromSwaps([swap.id]);
_swapStreamController.add(updatedSwap);
} catch (e, st) {
// Re-subscribe on error so watcher continues monitoring
_boltzRepo.subscribeToSwaps([swap.id]);
log.severe(
'{"swapId": "${swap.id}", "function": "_processReceiveLnToLiquidClaim"}',
error: e,
Expand Down Expand Up @@ -343,6 +353,10 @@ class SwapWatcherService {
swapType: swap.type,
);
final absoluteFeeOptions = networkFee.toAbsolute(txSize);

// Unsubscribe BEFORE refunding to prevent race condition with WebSocket updates
_boltzRepo.unsubscribeFromSwaps([swap.id]);

String refundTxid;
int actualFeesUsed;
log.fine(
Expand Down Expand Up @@ -383,8 +397,9 @@ class SwapWatcherService {
fees: swap.fees?.copyWith(claimFee: actualFeesUsed),
);
await _boltzRepo.updateSwap(swap: updatedSwap);
_boltzRepo.unsubscribeFromSwaps([swap.id]);
} catch (e, st) {
// Re-subscribe on error so watcher continues monitoring
_boltzRepo.subscribeToSwaps([swap.id]);
log.severe(
'{"swapId": "${swap.id}", "function": "_processSendLiquidToLnRefund"}',
error: e,
Expand Down Expand Up @@ -422,6 +437,10 @@ class SwapWatcherService {
swapType: swap.type,
);
final absoluteFeeOptions = networkFee.toAbsolute(txSize);

// Unsubscribe BEFORE refunding to prevent race condition with WebSocket updates
_boltzRepo.unsubscribeFromSwaps([swap.id]);

String refundTxid;
int actualFeesUsed;
log.fine(
Expand Down Expand Up @@ -462,8 +481,9 @@ class SwapWatcherService {
fees: swap.fees?.copyWith(claimFee: actualFeesUsed),
);
await _boltzRepo.updateSwap(swap: updatedSwap);
_boltzRepo.unsubscribeFromSwaps([swap.id]);
} catch (e, st) {
// Re-subscribe on error so watcher continues monitoring
_boltzRepo.subscribeToSwaps([swap.id]);
log.severe(
'{"swapId": "${swap.id}", "function": "_processSendBitcoinToLnRefund"}',
error: e,
Expand Down Expand Up @@ -506,6 +526,9 @@ class SwapWatcherService {
finalClaimAddress = swap.receiveAddress!;
}
}
// Unsubscribe BEFORE claiming to prevent race condition with WebSocket updates
_boltzRepo.unsubscribeFromSwaps([swap.id]);

String claimTxid;
log.fine(
'{"swapId": "${swap.id}", "function": "_processChainLiquidToBitcoinClaim", "action": "coop_claim_started", "timestamp": "${DateTime.now().toIso8601String()}"}',
Expand Down Expand Up @@ -537,8 +560,9 @@ class SwapWatcherService {
fees: swap.fees?.copyWith(claimFee: swap.fees!.claimFee),
);
await _boltzRepo.updateSwap(swap: updatedSwap);
_boltzRepo.unsubscribeFromSwaps([swap.id]);
} catch (e, st) {
// Re-subscribe on error so watcher continues monitoring
_boltzRepo.subscribeToSwaps([swap.id]);
log.severe(
'{"swapId": "${swap.id}", "function": "_processChainLiquidToBitcoinClaim"',
error: e,
Expand Down Expand Up @@ -584,6 +608,9 @@ class SwapWatcherService {
}
}

// Unsubscribe BEFORE claiming to prevent race condition with WebSocket updates
_boltzRepo.unsubscribeFromSwaps([swap.id]);

String claimTxid;
log.fine(
'{"swapId": "${swap.id}", "function": "_processChainBitcoinToLiquidClaim", "action": "coop_claim_started", "timestamp": "${DateTime.now().toIso8601String()}"}',
Expand Down Expand Up @@ -615,8 +642,9 @@ class SwapWatcherService {
fees: swap.fees?.copyWith(claimFee: swap.fees!.claimFee),
);
await _boltzRepo.updateSwap(swap: updatedSwap);
_boltzRepo.unsubscribeFromSwaps([swap.id]);
} catch (e, st) {
// Re-subscribe on error so watcher continues monitoring
_boltzRepo.subscribeToSwaps([swap.id]);
log.severe(
'{"swapId": "${swap.id}", "function": "_processChainBitcoinToLiquidClaim"}',
error: e,
Expand Down Expand Up @@ -658,6 +686,10 @@ class SwapWatcherService {
refundAddressForChainSwaps: refundAddress,
);
final absoluteFeeOptions = networkFee.toAbsolute(txSize);

// Unsubscribe BEFORE refunding to prevent race condition with WebSocket updates
_boltzRepo.unsubscribeFromSwaps([swap.id]);

String refundTxid;
int actualFeesUsed;
log.fine(
Expand Down Expand Up @@ -699,8 +731,9 @@ class SwapWatcherService {
fees: swap.fees?.copyWith(claimFee: actualFeesUsed),
);
await _boltzRepo.updateSwap(swap: updatedSwap);
_boltzRepo.unsubscribeFromSwaps([swap.id]);
} catch (e, st) {
// Re-subscribe on error so watcher continues monitoring
_boltzRepo.subscribeToSwaps([swap.id]);
log.severe(
'{"swapId": "${swap.id}", "function": "_processChainLiquidToBitcoinRefund"}',
error: e,
Expand Down Expand Up @@ -738,6 +771,10 @@ class SwapWatcherService {
refundAddressForChainSwaps: refundAddress,
);
final absoluteFeeOptions = networkFee.toAbsolute(txSize);

// Unsubscribe BEFORE refunding to prevent race condition with WebSocket updates
_boltzRepo.unsubscribeFromSwaps([swap.id]);

String refundTxid;
int actualFeesUsed;
log.fine(
Expand Down Expand Up @@ -779,8 +816,9 @@ class SwapWatcherService {
fees: swap.fees?.copyWith(claimFee: actualFeesUsed),
);
await _boltzRepo.updateSwap(swap: updatedSwap);
_boltzRepo.unsubscribeFromSwaps([swap.id]);
} catch (e, st) {
// Re-subscribe on error so watcher continues monitoring
_boltzRepo.subscribeToSwaps([swap.id]);
log.severe(
'{"swapId": "${swap.id}", "function": "_processChainBitcoinToLiquidRefund"}',
error: e,
Expand Down Expand Up @@ -810,19 +848,7 @@ class SwapWatcherService {
return;
case SwapType.liquidToBitcoin:
case SwapType.bitcoinToLiquid:
if (swap is ChainSwap &&
swap.receiveTxid == null &&
swap.refundTxid == null) {
if (swap.status == SwapStatus.claimable) {
final updatedSwap = swap.copyWith(status: SwapStatus.claimable);
await _boltzRepo.updateSwap(swap: updatedSwap);
} else if (swap.status == SwapStatus.refundable) {
final updatedSwap = swap.copyWith(status: SwapStatus.refundable);
await _boltzRepo.updateSwap(swap: updatedSwap);
}
} else {
return;
}
return;
}
} catch (e, st) {
log.severe(
Expand Down
23 changes: 11 additions & 12 deletions lib/core/widgets/loading/status_screen.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:bb_mobile/core/themes/app_theme.dart';
import 'package:bb_mobile/core/utils/build_context_x.dart';
import 'package:bb_mobile/core/widgets/buttons/button.dart';
import 'package:bb_mobile/core/widgets/loading/progress_screen.dart';
import 'package:bb_mobile/core/widgets/template/screen_template.dart';
Expand Down Expand Up @@ -36,18 +37,16 @@ class StatusScreen extends StatelessWidget {
return Scaffold(
backgroundColor: context.appColors.onSecondary,
body: StackedPage(
bottomChild:
(!isLoading && onTap != null)
? BBButton.big(
label:
hasError
? (buttonText ?? 'Try Again')
: (buttonText ?? 'Continue'),
onPressed: onTap ?? () {},
textColor: context.appColors.onPrimary,
bgColor: context.appColors.secondary,
)
: const SizedBox.shrink(),
bottomChild: (!isLoading && onTap != null)
? BBButton.big(
label: hasError
? (buttonText ?? context.loc.statusScreenTryAgain)
: (buttonText ?? context.loc.statusScreenContinue),
onPressed: onTap ?? () {},
textColor: context.appColors.onSecondary,
bgColor: context.appColors.secondary,
)
: const SizedBox.shrink(),
child: SafeArea(
child: SingleChildScrollView(
child: Padding(
Expand Down
12 changes: 12 additions & 0 deletions lib/core/widgets/template/screen_template.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:bb_mobile/core/themes/app_theme.dart';
import 'package:flutter/widgets.dart';

class StackedPage extends StatelessWidget {
Expand All @@ -24,6 +25,17 @@ class StackedPage extends StatelessWidget {
right: 16,
),
alignment: Alignment.bottomCenter,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
context.appColors.onSecondary.withValues(alpha: 0.0),
context.appColors.onSecondary,
],
stops: const [0.0, 0.3],
),
),
child: bottomChild,
),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ class BitcoinPriceBloc extends Bloc<BitcoinPriceEvent, BitcoinPriceState> {
try {
final settings = await _getSettingsUsecase.execute();
final currency = event.currency ?? settings.currencyCode;
final availableCurrencies =
await _getAvailableCurrenciesUsecase.execute();
final availableCurrencies = await _getAvailableCurrenciesUsecase
.execute();

final price = await _convertSatsToCurrencyAmountUsecase.execute(
currencyCode: currency,
Expand All @@ -74,11 +74,13 @@ class BitcoinPriceBloc extends Bloc<BitcoinPriceEvent, BitcoinPriceState> {
currency: currency,
availableCurrencies: availableCurrencies,
bitcoinPrice: price,
startupFailed: false,
error: null,
),
);
} catch (e) {
log.severe(e.toString());
emit(state.copyWith(error: e));
emit(state.copyWith(error: e, startupFailed: true));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ sealed class BitcoinPriceState with _$BitcoinPriceState {
const factory BitcoinPriceState({
@Default(false) bool loadingPrice,
Object? error,
@Default(false) bool startupFailed,
//
List<String>? availableCurrencies,
String? currency,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'price_chart_cubit.freezed.dart';
part 'price_chart_state.dart';

class PriceHistoryLoadException implements Exception {
const PriceHistoryLoadException();

@override
String toString() => 'PriceHistoryLoadException';
}

class PriceChartCubit extends Cubit<PriceChartState> {
PriceChartCubit({
required GetPriceHistoryUsecase getPriceHistoryUsecase,
Expand All @@ -28,9 +35,10 @@ class PriceChartCubit extends Cubit<PriceChartState> {
}) async {
emit(state.copyWith(isLoading: true, error: null));

String? selectedCurrency;
try {
final settings = await _getSettingsUsecase.execute();
final selectedCurrency = currency ?? settings.currencyCode;
selectedCurrency = currency ?? settings.currencyCode;

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

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

if (refreshedAllPrices.isNotEmpty || localAllPrices.isEmpty) {
if (refreshedAllPrices.isNotEmpty) {
emit(
state.copyWith(
isLoading: false,
Expand All @@ -96,9 +104,24 @@ class PriceChartCubit extends Cubit<PriceChartState> {
error: null,
),
);
} else if (localAllPrices.isEmpty) {
emit(
state.copyWith(
isLoading: false,
prices: [],
currency: selectedCurrency,
error: const PriceHistoryLoadException(),
),
);
}
} catch (e) {
emit(state.copyWith(isLoading: false, error: e));
emit(
state.copyWith(
isLoading: false,
error: e,
currency: selectedCurrency ?? state.currency,
),
);
}
}

Expand Down
29 changes: 29 additions & 0 deletions lib/features/bitcoin_price/ui/price_chart_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,35 @@ class PriceChartWidget extends StatelessWidget {
final rates = state.prices;
final hasNoLocalData = rates.isEmpty;

if (state.error != null && hasNoLocalData) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BBText(
context.loc.priceChartFailedToLoad,
style: context.font.bodyLarge?.copyWith(
color: context.appColors.onPrimary,
),
),
const Gap(16),
IconButton(
onPressed: () {
context.read<PriceChartCubit>().loadPriceHistory(
currency: state.currency,
);
},
icon: Icon(
Icons.refresh,
color: context.appColors.onPrimary,
size: 32,
),
),
],
),
);
}

if (state.isLoading || hasNoLocalData) {
if (state.isLoading) {
return Center(
Expand Down
Loading