diff --git a/lib/bloc/dex_repository.dart b/lib/bloc/dex_repository.dart index 45aa99f81d..4fea34095c 100644 --- a/lib/bloc/dex_repository.dart +++ b/lib/bloc/dex_repository.dart @@ -1,10 +1,10 @@ +import 'package:logging/logging.dart' show Logger; import 'package:rational/rational.dart'; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/mm2/mm2_api/rpc/best_orders/best_orders.dart'; import 'package:web_dex/mm2/mm2_api/rpc/best_orders/best_orders_request.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/setprice/setprice_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/max_taker_vol/max_taker_vol_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/max_taker_vol/max_taker_vol_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/min_trading_vol/min_trading_vol.dart'; @@ -13,6 +13,7 @@ import 'package:web_dex/mm2/mm2_api/rpc/my_recent_swaps/my_recent_swaps_request. import 'package:web_dex/mm2/mm2_api/rpc/my_swap_status/my_swap_status_req.dart'; import 'package:web_dex/mm2/mm2_api/rpc/sell/sell_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/sell/sell_response.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/setprice/setprice_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trade_preimage/trade_preimage_errors.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trade_preimage/trade_preimage_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trade_preimage/trade_preimage_response.dart'; @@ -21,12 +22,13 @@ import 'package:web_dex/model/swap.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'; -import 'package:web_dex/shared/utils/utils.dart'; +import 'package:web_dex/shared/utils/utils.dart' show fract2rat; class DexRepository { DexRepository(this._mm2Api); final Mm2Api _mm2Api; + final _log = Logger('DexRepository'); Future sell(SellRequest request) async { try { @@ -37,17 +39,27 @@ class DexRepository { } } - Future?> setPrice(SetPriceRequest request) async { + Future setPrice(SetPriceRequest request) async { try { - return await _mm2Api.setprice(request); + final Map? response = await _mm2Api.setprice(request); + if (response == null) { + throw Exception('Null response from setprice'); + } + + final Object? error = response['error']; + if (error != null) { + throw Exception(error.toString()); + } + + final String? uuid = response['result']?['uuid'] as String?; + if (uuid == null) { + throw Exception('Missing uuid in setprice response'); + } + + return uuid; } catch (e, s) { - log( - 'Error setprice ${request.base}/${request.rel}: $e', - path: 'dex_repository => setPrice', - trace: s, - isError: true, - ).ignore(); - return {'error': e.toString()}; + _log.severe('Error setprice ${request.base}/${request.rel}', e, s); + rethrow; } } @@ -89,12 +101,10 @@ class DexRepository { ), ); } catch (e, s) { - log( - e.toString(), - path: - 'swaps_service => getTradePreimage => mapTradePreimageResponseToTradePreimage', - trace: s, - isError: true, + _log.severe( + 'swaps_service => getTradePreimage => mapTradePreimageResponseToTradePreimage', + e, + s, ); return DataFromService(error: TextError(error: 'Something wrong')); } @@ -148,7 +158,7 @@ class DexRepository { try { return BestOrders.fromJson(response!); } catch (e, s) { - log('Error parsing best_orders response: $e', trace: s, isError: true); + _log.severe('Error parsing best_orders response', e, s); return BestOrders( error: TextError( diff --git a/lib/bloc/taker_form/taker_bloc.dart b/lib/bloc/taker_form/taker_bloc.dart index dc16c612d7..b00700e03c 100644 --- a/lib/bloc/taker_form/taker_bloc.dart +++ b/lib/bloc/taker_form/taker_bloc.dart @@ -4,6 +4,7 @@ import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:logging/logging.dart'; import 'package:rational/rational.dart'; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; @@ -24,7 +25,6 @@ import 'package:web_dex/model/data_from_service.dart'; import 'package:web_dex/model/dex_form_error.dart'; import 'package:web_dex/model/text_error.dart'; import 'package:web_dex/model/trade_preimage.dart'; -import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/views/dex/dex_helpers.dart'; class TakerBloc extends Bloc { @@ -80,6 +80,7 @@ class TakerBloc extends Bloc { final DexRepository _dexRepo; final CoinsRepo _coinsRepo; + final _log = Logger('TakerBloc'); Timer? _maxSellAmountTimer; bool _activatingAssets = false; bool _waitingForWallet = true; @@ -88,99 +89,122 @@ class TakerBloc extends Bloc { late StreamSubscription _authorizationSubscription; Future _onStartSwap( - TakerStartSwap event, Emitter emit) async { - emit(state.copyWith( - inProgress: () => true, - )); + TakerStartSwap event, + Emitter emit, + ) async { + emit( + state.copyWith( + inProgress: () => true, + ), + ); final base = state.sellCoin!.abbr; final rel = state.selectedOrder!.coin; - BestOrders fetchedOrders = await _dexRepo.getBestOrders( - BestOrdersRequest( - coin: base, - type: BestOrdersRequestType.number, - number: 40, - action: 'sell', - ), - ); + try { + final fetchedOrders = await _dexRepo.getBestOrders( + BestOrdersRequest( + coin: base, + type: BestOrdersRequestType.number, + number: 10, + action: 'sell', + ), + ); - bool hasMatchingOrder = false; - final ordersForRel = fetchedOrders.result?[rel]; - if (ordersForRel != null) { - hasMatchingOrder = - ordersForRel.any((order) => order.uuid == state.selectedOrder!.uuid); - } + bool hasMatchingOrder = false; + final ordersForRel = fetchedOrders.result?[rel]; + if (ordersForRel != null) { + hasMatchingOrder = ordersForRel.any( + (order) => + order.uuid == state.selectedOrder!.uuid && + order.price == state.selectedOrder!.price && + order.maxVolume >= state.sellAmount!, + ); + } + + if (!hasMatchingOrder) { + _log.info('No matching order found, placing maker order'); + final String uuid = await _dexRepo.setPrice( + SetPriceRequest( + base: base, + rel: rel, + volume: state.sellAmount!, + price: state.selectedOrder!.price, + ), + ); + emit(state.copyWith(swapUuid: () => uuid)); + return; + } - if (!hasMatchingOrder) { - final response = await _dexRepo.setPrice( - SetPriceRequest( + final SellResponse response = await _dexRepo.sell( + SellRequest( base: base, rel: rel, volume: state.sellAmount!, price: state.selectedOrder!.price, + orderType: SellBuyOrderType.fillOrKill, ), ); - final String? uuid = response?['result']?['uuid'] as String?; - if (response == null || response['error'] != null || uuid == null) { - add(TakerAddError(DexFormError( - error: response?['error']?.toString() ?? 'Order failed'))); + + if (response.error != null) { + add(TakerAddError(DexFormError(error: response.error!.message))); + } + + final String? uuid = response.result?.uuid; + if (uuid == null || uuid.isEmpty) { + const error = 'Failed to start swap: no UUID returned from sell'; + _log.severe(error); + add(TakerAddError(DexFormError(error: error))); emit(state.copyWith(inProgress: () => false)); return; } - emit(state.copyWith(swapUuid: () => uuid)); - return; - } - - final SellResponse response = await _dexRepo.sell( - SellRequest( - base: base, - rel: rel, - volume: state.sellAmount!, - price: state.selectedOrder!.price, - orderType: SellBuyOrderType.fillOrKill, - ), - ); - if (response.error != null) { - add(TakerAddError(DexFormError(error: response.error!.message))); + emit( + state.copyWith( + inProgress: () => false, + swapUuid: () => uuid, + ), + ); + } catch (e, s) { + _log.severe('Failed to start swap', e, s); + add(TakerAddError(DexFormError(error: e.toString()))); + emit(state.copyWith(inProgress: () => false)); } - - final String? uuid = response.result?.uuid; - - emit(state.copyWith( - inProgress: uuid == null ? () => false : null, - swapUuid: () => uuid, - )); } void _onBackButtonClick( TakerBackButtonClick event, Emitter emit, ) { - emit(state.copyWith( - step: () => TakerStep.form, - errors: () => [], - )); + emit( + state.copyWith( + step: () => TakerStep.form, + errors: () => [], + ), + ); } Future _onFormSubmitClick( TakerFormSubmitClick event, Emitter emit, ) async { - emit(state.copyWith( - inProgress: () => true, - autovalidate: () => true, - )); + emit( + state.copyWith( + inProgress: () => true, + autovalidate: () => true, + ), + ); - await pauseWhile(() => _waitingForWallet || _activatingAssets); + await _pauseWhile(() => _waitingForWallet || _activatingAssets); final bool isValid = await _validator.validate(); - emit(state.copyWith( - inProgress: () => false, - step: () => isValid ? TakerStep.confirm : TakerStep.form, - )); + emit( + state.copyWith( + inProgress: () => false, + step: () => isValid ? TakerStep.confirm : TakerStep.form, + ), + ); } void _onAmountButtonClick( @@ -212,13 +236,15 @@ class TakerBloc extends Bloc { TakerSetSellAmount event, Emitter emit, ) { - emit(state.copyWith( - sellAmount: () => event.amount, - buyAmount: () => calculateBuyAmount( - selectedOrder: state.selectedOrder, - sellAmount: event.amount, + emit( + state.copyWith( + sellAmount: () => event.amount, + buyAmount: () => calculateBuyAmount( + selectedOrder: state.selectedOrder, + sellAmount: event.amount, + ), ), - )); + ); if (state.autovalidate) { _validator.validateForm(); @@ -235,18 +261,22 @@ class TakerBloc extends Bloc { final List errorsList = List.from(state.errors); errorsList.add(event.error); - emit(state.copyWith( - errors: () => errorsList, - )); + emit( + state.copyWith( + errors: () => errorsList, + ), + ); } void _onClearErrors( TakerClearErrors event, Emitter emit, ) { - emit(state.copyWith( - errors: () => [], - )); + emit( + state.copyWith( + errors: () => [], + ), + ); } Future _onSelectOrder( @@ -257,17 +287,19 @@ class TakerBloc extends Bloc { event.order != null && state.selectedOrder!.coin != event.order!.coin; - emit(state.copyWith( - selectedOrder: () => event.order, - showOrderSelector: () => false, - buyAmount: () => calculateBuyAmount( - sellAmount: state.sellAmount, - selectedOrder: event.order, + emit( + state.copyWith( + selectedOrder: () => event.order, + showOrderSelector: () => false, + buyAmount: () => calculateBuyAmount( + sellAmount: state.sellAmount, + selectedOrder: event.order, + ), + tradePreimage: () => null, + errors: () => [], + autovalidate: switchingCoin ? () => false : null, ), - tradePreimage: () => null, - errors: () => [], - autovalidate: switchingCoin ? () => false : null, - )); + ); if (!state.autovalidate) add(TakerVerifyOrderVolume()); @@ -292,20 +324,22 @@ class TakerBloc extends Bloc { ) async { if (event.setOnlyIfNotSet && state.sellCoin != null) return; - emit(state.copyWith( - sellCoin: () => event.coin, - showCoinSelector: () => false, - selectedOrder: () => null, - bestOrders: () => null, - sellAmount: () => null, - buyAmount: () => null, - tradePreimage: () => null, - maxSellAmount: () => null, - minSellAmount: () => null, - errors: () => [], - autovalidate: () => false, - availableBalanceState: () => AvailableBalanceState.initial, - )); + emit( + state.copyWith( + sellCoin: () => event.coin, + showCoinSelector: () => false, + selectedOrder: () => null, + bestOrders: () => null, + sellAmount: () => null, + buyAmount: () => null, + tradePreimage: () => null, + maxSellAmount: () => null, + minSellAmount: () => null, + errors: () => [], + autovalidate: () => false, + availableBalanceState: () => AvailableBalanceState.initial, + ), + ); add(TakerUpdateBestOrders(autoSelectOrderAbbr: event.autoSelectOrderAbbr)); @@ -320,9 +354,11 @@ class TakerBloc extends Bloc { ) async { final Coin? coin = state.sellCoin; - emit(state.copyWith( - bestOrders: () => null, - )); + emit( + state.copyWith( + bestOrders: () => null, + ), + ); if (coin == null) return; @@ -355,10 +391,12 @@ class TakerBloc extends Bloc { TakerCoinSelectorClick event, Emitter emit, ) { - emit(state.copyWith( - showCoinSelector: () => !state.showCoinSelector, - showOrderSelector: () => false, - )); + emit( + state.copyWith( + showCoinSelector: () => !state.showCoinSelector, + showOrderSelector: () => false, + ), + ); } Future _onOrderSelectorClick( @@ -370,11 +408,13 @@ class TakerBloc extends Bloc { return; } - emit(state.copyWith( - showOrderSelector: () => !state.showOrderSelector, - showCoinSelector: () => false, - bestOrders: _haveBestOrders ? () => state.bestOrders : () => null, - )); + emit( + state.copyWith( + showOrderSelector: () => !state.showOrderSelector, + showCoinSelector: () => false, + bestOrders: _haveBestOrders ? () => state.bestOrders : () => null, + ), + ); if (state.showOrderSelector && !_haveBestOrders) { add(TakerUpdateBestOrders()); @@ -391,18 +431,22 @@ class TakerBloc extends Bloc { TakerCoinSelectorOpen event, Emitter emit, ) { - emit(state.copyWith( - showCoinSelector: () => event.isOpen, - )); + emit( + state.copyWith( + showCoinSelector: () => event.isOpen, + ), + ); } void _onOrderSelectorOpen( TakerOrderSelectorOpen event, Emitter emit, ) { - emit(state.copyWith( - showOrderSelector: () => event.isOpen, - )); + emit( + state.copyWith( + showOrderSelector: () => event.isOpen, + ), + ); } void _onClear( @@ -411,9 +455,11 @@ class TakerBloc extends Bloc { ) { _maxSellAmountTimer?.cancel(); - emit(TakerState.initial().copyWith( - availableBalanceState: () => AvailableBalanceState.unavailable, - )); + emit( + TakerState.initial().copyWith( + availableBalanceState: () => AvailableBalanceState.unavailable, + ), + ); } void _subscribeMaxSellAmount() { @@ -435,29 +481,39 @@ class TakerBloc extends Bloc { } if (state.availableBalanceState == AvailableBalanceState.initial || event.setLoadingStatus) { - emitter(state.copyWith( - availableBalanceState: () => AvailableBalanceState.loading)); + emitter( + state.copyWith( + availableBalanceState: () => AvailableBalanceState.loading, + ), + ); } if (!_isLoggedIn) { - emitter(state.copyWith( - availableBalanceState: () => AvailableBalanceState.unavailable)); + emitter( + state.copyWith( + availableBalanceState: () => AvailableBalanceState.unavailable, + ), + ); } else { Rational? maxSellAmount = await _dexRepo.getMaxTakerVolume(state.sellCoin!.abbr); if (maxSellAmount != null) { - emitter(state.copyWith( - maxSellAmount: () => maxSellAmount, - availableBalanceState: () => AvailableBalanceState.success, - )); + emitter( + state.copyWith( + maxSellAmount: () => maxSellAmount, + availableBalanceState: () => AvailableBalanceState.success, + ), + ); } else { maxSellAmount = await _frequentlyGetMaxTakerVolume(); - emitter(state.copyWith( - maxSellAmount: () => maxSellAmount, - availableBalanceState: maxSellAmount == null - ? () => AvailableBalanceState.failure - : () => AvailableBalanceState.success, - )); + emitter( + state.copyWith( + maxSellAmount: () => maxSellAmount, + availableBalanceState: maxSellAmount == null + ? () => AvailableBalanceState.failure + : () => AvailableBalanceState.success, + ), + ); } } } @@ -471,7 +527,7 @@ class TakerBloc extends Bloc { return maxSellAmount; } attempts -= 1; - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(const Duration(seconds: 2)); } return null; } @@ -482,27 +538,33 @@ class TakerBloc extends Bloc { ) async { if (state.sellCoin == null) return; if (!_isLoggedIn) { - emit(state.copyWith( - minSellAmount: () => null, - )); + emit( + state.copyWith( + minSellAmount: () => null, + ), + ); return; } final Rational? minSellAmount = await _dexRepo.getMinTradingVolume(state.sellCoin!.abbr); - emit(state.copyWith( - minSellAmount: () => minSellAmount, - )); + emit( + state.copyWith( + minSellAmount: () => minSellAmount, + ), + ); } Future _onUpdateFees( TakerUpdateFees event, Emitter emit, ) async { - emit(state.copyWith( - tradePreimage: () => null, - )); + emit( + state.copyWith( + tradePreimage: () => null, + ), + ); if (!_validator.canRequestPreimage) return; @@ -527,8 +589,7 @@ class TakerBloc extends Bloc { state.sellAmount, ); } catch (e, s) { - log(e.toString(), - trace: s, path: 'taker_bloc::_getFeesData', isError: true); + _log.severe('Failed to get trade preimage', e, s); return DataFromService(error: TextError(error: 'Failed to request fees')); } } @@ -550,9 +611,11 @@ class TakerBloc extends Bloc { TakerSetInProgress event, Emitter emit, ) { - emit(state.copyWith( - inProgress: () => event.value, - )); + emit( + state.copyWith( + inProgress: () => event.value, + ), + ); } void _onSetWalletReady( @@ -570,10 +633,12 @@ class TakerBloc extends Bloc { } Future _onReInit(TakerReInit event, Emitter emit) async { - emit(state.copyWith( - errors: () => [], - autovalidate: () => false, - )); + emit( + state.copyWith( + errors: () => [], + autovalidate: () => false, + ), + ); await _autoActivateCoin(state.sellCoin?.abbr); await _autoActivateCoin(state.selectedOrder?.coin); } @@ -586,3 +651,35 @@ class TakerBloc extends Bloc { return super.close(); } } + +/// Pauses the execution of the current function until the given condition +/// is false or the timeout is reached. +/// +/// This is useful for waiting for asynchronous operations to complete +/// or for certain conditions to be met before proceeding. +/// +/// /// [condition] is a function that returns a boolean indicating whether +/// the condition is still true. +/// [timeout] is the maximum duration to wait before giving up. +/// If the condition is still true after the timeout, +/// the function will stop waiting and return. +/// The function will periodically check the condition every 10 milliseconds +/// until the condition is false or the timeout is reached. +/// Note: This function is not cancellable and will block the current +/// execution context until the condition is false or the timeout is reached. +/// Example usage: +/// ```dart +/// await _pauseWhile(() => someCondition, timeout: Duration(seconds: 5)); +/// ``` +Future _pauseWhile( + bool Function() condition, { + Duration timeout = const Duration(seconds: 30), +}) async { + final int startMs = DateTime.now().millisecondsSinceEpoch; + bool timedOut = false; + while (condition() && !timedOut) { + await Future.delayed(const Duration(milliseconds: 10)); + timedOut = DateTime.now().millisecondsSinceEpoch - startMs > + timeout.inMilliseconds; + } +}