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
2 changes: 2 additions & 0 deletions assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,8 @@
"orderBookNoAsks": "No asks found",
"orderBookNoBids": "No bids found",
"orderBookEmpty": "Orderbook is empty",
"dexNoSwapOffers": "No swap offers available for the selected asset.",
"bridgeNoCrossNetworkRoutes": "No cross-network routes found for this asset.",
"freshAddress": "Fresh address",
"userActionRequired": "User action required",
"unknown": "Unknown",
Expand Down
5 changes: 5 additions & 0 deletions lib/app_config/app_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ const Duration kPerformanceLogInterval = Duration(minutes: 1);
/// - Balance and price update polling
const bool kDebugElectrumLogs = true;

/// Temporary failure simulation toggles for testing UI/flows.
/// Guarded by kDebugMode in calling sites.
const bool kSimulateBestOrdersFailure = false;
const double kSimulatedBestOrdersFailureRate = 0.5; // 50%

// This information is here because it is not contextual and is branded.
// Names of their own are not localized. Also, the application is initialized before
// the localization package is initialized.
Expand Down
28 changes: 22 additions & 6 deletions lib/bloc/bridge_form/bridge_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,17 @@ class BridgeBloc extends Bloc<BridgeEvent, BridgeState> {
),
);

// Before login, show 0.00 instead of spinner
if (!_isLoggedIn) {
emit(
state.copyWith(
availableBalanceState: () => AvailableBalanceState.unavailable,
maxSellAmount: () => null,
),
);
return;
}

_autoActivateCoin(event.coin.abbr);
_subscribeMaxSellAmount();

Expand Down Expand Up @@ -392,22 +403,27 @@ class BridgeBloc extends Bloc<BridgeEvent, BridgeState> {
return;
}

if (state.availableBalanceState == AvailableBalanceState.initial ||
event.setLoadingStatus) {
// If not logged in, show 0.00 (unavailable) and skip spinner
if (!_isLoggedIn) {
emit(
state.copyWith(
availableBalanceState: () => AvailableBalanceState.loading,
availableBalanceState: () => AvailableBalanceState.unavailable,
maxSellAmount: () => null,
),
);
return;
}

if (!_isLoggedIn) {
if (state.availableBalanceState == AvailableBalanceState.initial ||
event.setLoadingStatus) {
emit(
state.copyWith(
availableBalanceState: () => AvailableBalanceState.unavailable,
availableBalanceState: () => AvailableBalanceState.loading,
),
);
} else {
}

{
Rational? maxSellAmount = await _dexRepository.getMaxTakerVolume(
state.sellCoin!.abbr,
);
Expand Down
107 changes: 86 additions & 21 deletions lib/bloc/dex_repository.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:rational/rational.dart';
import 'package:web_dex/app_config/app_config.dart';
import 'package:web_dex/mm2/mm2_api/mm2_api.dart';
Expand All @@ -19,6 +21,8 @@ import 'package:web_dex/mm2/mm2_api/rpc/trade_preimage/trade_preimage_request.da
import 'package:web_dex/mm2/mm2_api/rpc/trade_preimage/trade_preimage_response.dart';
import 'package:web_dex/model/data_from_service.dart';
import 'package:web_dex/model/swap.dart';
import 'package:web_dex/router/state/routing_state.dart';
import 'package:web_dex/model/main_menu_value.dart';
import 'package:web_dex/model/text_error.dart';
import 'package:web_dex/model/trade_preimage.dart';
import 'package:web_dex/services/mappers/trade_preimage_mappers.dart';
Expand Down Expand Up @@ -54,9 +58,12 @@ class DexRepository {
swapMethod: swapMethod,
max: max,
);
final ApiResponse<TradePreimageRequest, TradePreimageResponseResult,
Map<String, dynamic>> response =
await _mm2Api.getTradePreimage(request);
final ApiResponse<
TradePreimageRequest,
TradePreimageResponseResult,
Map<String, dynamic>
>
response = await _mm2Api.getTradePreimage(request);

final Map<String, dynamic>? error = response.error;
final TradePreimageResponseResult? result = response.result;
Expand Down Expand Up @@ -88,8 +95,9 @@ class DexRepository {
}

Future<Rational?> getMaxTakerVolume(String coinAbbr) async {
final MaxTakerVolResponse? response =
await _mm2Api.getMaxTakerVolume(MaxTakerVolRequest(coin: coinAbbr));
final MaxTakerVolResponse? response = await _mm2Api.getMaxTakerVolume(
MaxTakerVolRequest(coin: coinAbbr),
);
if (response == null) {
return null;
}
Expand All @@ -98,8 +106,9 @@ class DexRepository {
}

Future<Rational?> getMaxMakerVolume(String coinAbbr) async {
final MaxMakerVolResponse? response =
await _mm2Api.getMaxMakerVolume(MaxMakerVolRequest(coin: coinAbbr));
final MaxMakerVolResponse? response = await _mm2Api.getMaxMakerVolume(
MaxMakerVolRequest(coin: coinAbbr),
);
if (response == null) {
return null;
}
Expand All @@ -108,8 +117,9 @@ class DexRepository {
}

Future<Rational?> getMinTradingVolume(String coinAbbr) async {
final MinTradingVolResponse? response =
await _mm2Api.getMinTradingVol(MinTradingVolRequest(coin: coinAbbr));
final MinTradingVolResponse? response = await _mm2Api.getMinTradingVol(
MinTradingVolRequest(coin: coinAbbr),
);
if (response == null) {
return null;
}
Expand All @@ -122,28 +132,82 @@ class DexRepository {
}

Future<BestOrders> getBestOrders(BestOrdersRequest request) async {
// Only allow best_orders when user is on Swap (DEX) or Bridge pages
final MainMenuValue current = routingState.selectedMenu;
final bool isTradingPage =
current == MainMenuValue.dex || current == MainMenuValue.bridge;
if (!isTradingPage) {
// Not an error – we intentionally suppress best_orders away from trading pages
return BestOrders(result: <String, List<BestOrder>>{});
}

// Testing aid: opt-in random failure in debug mode
if (kDebugMode &&
kSimulateBestOrdersFailure &&
Random().nextDouble() < kSimulatedBestOrdersFailureRate) {
return BestOrders(
error: TextError(error: 'Simulated best_orders failure (debug)'),
);
}

Map<String, dynamic>? response;
try {
response = await _mm2Api.getBestOrders(request);
} catch (e) {
} catch (e, s) {
log(
'best_orders request failed: $e',
trace: s,
path: 'api => getBestOrders',
isError: true,
).ignore();
return BestOrders(error: TextError.fromString(e.toString()));
}

final isErrorResponse =
(response?['error'] as String?)?.isNotEmpty ?? false;
final hasResult =
(response?['result'] as Map<String, dynamic>?)?.isNotEmpty ?? false;
if (response == null) {
return BestOrders(
error: TextError(error: 'best_orders returned null response'),
);
}

final String? errorText = response['error'] as String?;
if (errorText != null && errorText.isNotEmpty) {
// Map known "no orders" network condition to empty result so UI shows a
// graceful "Nothing found" instead of an error panel.
final String? errorType = response['error_type'] as String?;
final String? errorPath = response['error_path'] as String?;
final bool isNoOrdersNetworkCondition =
errorPath == 'best_orders' &&
errorType == 'P2PError' &&
errorText.contains('No response from any peer');

if (isErrorResponse) {
return BestOrders(error: TextError(error: response!['error']!));
// Mm2Api.getBestOrders may wrap MM2 errors in an Exception() during
// retry handling, yielding text like: "Exception: No response from any peer"
// (without error_type/error_path). Treat these as "no orders" as well.
final bool isWrappedNoOrdersText = errorText.toLowerCase().contains(
'no response from any peer',
);

if (isNoOrdersNetworkCondition || isWrappedNoOrdersText) {
return BestOrders(result: <String, List<BestOrder>>{});
}

log(
'best_orders returned error: $errorText',
path: 'api => getBestOrders',
isError: true,
).ignore();
return BestOrders(error: TextError(error: errorText));
}

if (!hasResult) {
return BestOrders(error: TextError(error: 'Orders not found!'));
final Map<String, dynamic>? result =
response['result'] as Map<String, dynamic>?;
if (result == null || result.isEmpty) {
// No error and no result → no liquidity available
return BestOrders(result: <String, List<BestOrder>>{});
}

try {
return BestOrders.fromJson(response!);
return BestOrders.fromJson(response);
} catch (e, s) {
log('Error parsing best_orders response: $e', trace: s, isError: true);

Expand All @@ -156,8 +220,9 @@ class DexRepository {
}

Future<Swap> getSwapStatus(String swapUuid) async {
final response =
await _mm2Api.getSwapStatus(MySwapStatusReq(uuid: swapUuid));
final response = await _mm2Api.getSwapStatus(
MySwapStatusReq(uuid: swapUuid),
);

if (response['error'] != null) {
throw TextError(error: response['error']);
Expand Down
58 changes: 36 additions & 22 deletions lib/bloc/taker_form/taker_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,17 @@ class TakerBloc extends Bloc<TakerEvent, TakerState> {

add(TakerUpdateBestOrders(autoSelectOrderAbbr: event.autoSelectOrderAbbr));

// Before login, show 0.00 instead of spinner
if (!_isLoggedIn) {
emit(
state.copyWith(
availableBalanceState: () => AvailableBalanceState.unavailable,
maxSellAmount: () => null,
),
);
return;
}

await _autoActivateCoin(state.sellCoin?.abbr);
_subscribeMaxSellAmount();
add(TakerGetMinSellAmount());
Expand Down Expand Up @@ -440,6 +451,17 @@ class TakerBloc extends Bloc<TakerEvent, TakerState> {
_maxSellAmountTimer?.cancel();
return;
}
// If not logged in, show 0.00 (unavailable) and skip spinner
if (!_isLoggedIn) {
emitter(
state.copyWith(
availableBalanceState: () => AvailableBalanceState.unavailable,
maxSellAmount: () => null,
),
);
return;
}

if (state.availableBalanceState == AvailableBalanceState.initial ||
event.setLoadingStatus) {
emitter(
Expand Down Expand Up @@ -469,34 +491,26 @@ class TakerBloc extends Bloc<TakerEvent, TakerState> {
return;
}

if (!_isLoggedIn) {
Rational? maxSellAmount = await _dexRepo.getMaxTakerVolume(
state.sellCoin!.abbr,
);
if (maxSellAmount != null) {
emitter(
state.copyWith(
availableBalanceState: () => AvailableBalanceState.unavailable,
maxSellAmount: () => maxSellAmount,
availableBalanceState: () => AvailableBalanceState.success,
),
);
} else {
Rational? maxSellAmount = await _dexRepo.getMaxTakerVolume(
state.sellCoin!.abbr,
maxSellAmount = await _frequentlyGetMaxTakerVolume();
emitter(
state.copyWith(
maxSellAmount: () => maxSellAmount,
availableBalanceState: maxSellAmount == null
? () => AvailableBalanceState.failure
: () => AvailableBalanceState.success,
),
);
if (maxSellAmount != null) {
emitter(
state.copyWith(
maxSellAmount: () => maxSellAmount,
availableBalanceState: () => AvailableBalanceState.success,
),
);
} else {
maxSellAmount = await _frequentlyGetMaxTakerVolume();
emitter(
state.copyWith(
maxSellAmount: () => maxSellAmount,
availableBalanceState: maxSellAmount == null
? () => AvailableBalanceState.failure
: () => AvailableBalanceState.success,
),
);
}
}
} catch (e, s) {
_log.severe('Failed to update max sell amount', e, s);
Expand Down
13 changes: 8 additions & 5 deletions lib/blocs/maker_form_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,10 @@ class MakerFormBloc implements BlocBase {
Future<void> _updateMaxSellAmountListener() async {
_maxSellAmountTimer?.cancel();
maxSellAmount = null;
availableBalanceState = AvailableBalanceState.loading;
// Only show loading spinner when signed in
final bool isSignedIn = await kdfSdk.auth.isSignedIn();
availableBalanceState =
isSignedIn ? AvailableBalanceState.loading : AvailableBalanceState.unavailable;
isMaxActive = false;
Comment thread
smk762 marked this conversation as resolved.

await _updateMaxSellAmount();
Expand All @@ -259,17 +262,17 @@ class MakerFormBloc implements BlocBase {

Future<void> _updateMaxSellAmount() async {
final Coin? coin = sellCoin;
if (availableBalanceState == AvailableBalanceState.initial) {
availableBalanceState = AvailableBalanceState.loading;
}

final bool isSignedIn = await kdfSdk.auth.isSignedIn();
if (!isSignedIn) {
maxSellAmount = null;
availableBalanceState = AvailableBalanceState.unavailable;
return;
}

if (availableBalanceState == AvailableBalanceState.initial) {
availableBalanceState = AvailableBalanceState.loading;
}

if (coin == null) {
maxSellAmount = null;
availableBalanceState = AvailableBalanceState.unavailable;
Expand Down
2 changes: 2 additions & 0 deletions lib/generated/codegen_loader.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,8 @@ abstract class LocaleKeys {
static const orderBookNoAsks = 'orderBookNoAsks';
static const orderBookNoBids = 'orderBookNoBids';
static const orderBookEmpty = 'orderBookEmpty';
static const dexNoSwapOffers = 'dexNoSwapOffers';
static const bridgeNoCrossNetworkRoutes = 'bridgeNoCrossNetworkRoutes';
static const freshAddress = 'freshAddress';
static const userActionRequired = 'userActionRequired';
static const unknown = 'unknown';
Expand Down
Loading
Loading