Cw 171 exchange auto selector bug (#523)

* Fix de-selecting exchange providers not getting saved * only change field value when the amount is better than the already existing one * Show calculated amount after getting the best value from all providers * Catch exceptions to avoid stopping the providers calculate amount APIs * Fix Splay map only saving the last value obtained * Show limits for the provider with the highest rate * Load limits on currency pair change * Show limits for lowest min provider * Show limits for lowest min and highest max * Sync best rate every 10 seconds instead of calculating it on every amount change * Fix conflicts with flutter upgrade Add null safety to changes in exchange_view_model.dart * Remove un-necessary checks

Omar Hatem committed Oct 20, 2022 at 19:47 UTC 7e7217008cde05486cd8bdad8dd934ae453c378b
4 files changed +149 -137
lib/exchange/simpleswap/simpleswap_exchange_provider.dart
+3 -3
@@ -56,7 +56,7 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
56 final uri = Uri.https(apiAuthority, getEstimatePath, params);
57 final response = await get(uri);
58
59 - if (response.body == null) return 0.00;
59 + if (response.body == null || response.body == "null") return 0.00;
60 final data = json.decode(response.body) as String;
61
62 return double.parse(data);
@@ -151,8 +151,8 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
151 }
152
153 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
154 - final min = responseJSON['min'] != null ? double.tryParse(responseJSON['min'] as String) : null;
155 - final max = responseJSON['max'] != null ? double.parse(responseJSON['max'] as String) : null;
154 + final min = double.tryParse(responseJSON['min'] as String? ?? '');
155 + final max = double.tryParse(responseJSON['max'] as String? ?? '');
156
157 return Limits(min: min, max: max);
158 }
lib/src/screens/exchange/exchange_page.dart
+7 -3
@@ -179,6 +179,7 @@ class ExchangePage extends BasePage {
179 padding: EdgeInsets.fromLTRB(24, 100, 24, 32),
180 child: Observer(
181 builder: (_) => ExchangeCard(
182 + onDispose: disposeBestRateSync,
183 hasAllAmount: exchangeViewModel.hasAllAmount,
184 allAmount: exchangeViewModel.hasAllAmount
185 ? () => exchangeViewModel
@@ -265,6 +266,7 @@ class ExchangePage extends BasePage {
266 EdgeInsets.only(top: 29, left: 24, right: 24),
267 child: Observer(
268 builder: (_) => ExchangeCard(
269 + onDispose: disposeBestRateSync,
270 amountFocusNode: _receiveAmountFocus,
271 addressFocusNode: _receiveAddressFocus,
272 key: receiveKey,
@@ -743,13 +745,13 @@ class ExchangePage extends BasePage {
745 if (_receiveAmountFocus.hasFocus) {
746 exchangeViewModel.isFixedRateMode = true;
747 }
746 - exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
748 + // exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
749 });
750
751 _depositAmountFocus.addListener(() {
752 exchangeViewModel.isFixedRateMode = false;
751 - exchangeViewModel.changeDepositAmount(
752 - amount: depositAmountController.text);
753 + // exchangeViewModel.changeDepositAmount(
754 + // amount: depositAmountController.text);
755 });
756
757 _isReactionsSet = true;
@@ -791,4 +793,6 @@ class ExchangePage extends BasePage {
793 final address = await extractAddressFromParsed(context, parsedAddress);
794 return address;
795 }
796 +
797 + void disposeBestRateSync() => exchangeViewModel.bestRateSync?.cancel();
798 }
lib/src/screens/exchange/widgets/exchange_card.dart
+10 -1
@@ -37,7 +37,8 @@ class ExchangeCard extends StatefulWidget {
37 this.addressFocusNode,
38 this.allAmount,
39 this.onPushPasteButton,
40 - this.onPushAddressBookButton})
40 + this.onPushAddressBookButton,
41 + this.onDispose})
42 : super(key: key);
43
44 final List<CryptoCurrency> currencies;
@@ -63,6 +64,7 @@ class ExchangeCard extends StatefulWidget {
64 final VoidCallback? allAmount;
65 final void Function(BuildContext context)? onPushPasteButton;
66 final void Function(BuildContext context)? onPushAddressBookButton;
67 + final Function()? onDispose;
68
69 @override
70 ExchangeCardState createState() => ExchangeCardState();
@@ -106,6 +108,13 @@ class ExchangeCardState extends State<ExchangeCard> {
108 super.initState();
109 }
110
111 + @override
112 + void dispose() {
113 + widget.onDispose?.call();
114 +
115 + super.dispose();
116 + }
117 +
118 void changeLimits({String? min, String? max}) {
119 setState(() {
120 _min = min;
lib/view_model/exchange/exchange_view_model.dart
+129 -130
@@ -1,3 +1,4 @@
1 +import 'dart:async';
2 import 'dart:collection';
3 import 'dart:convert';
4
@@ -59,12 +60,12 @@ abstract class ExchangeViewModelBase with Store {
60 receiveCurrency = wallet.currency,
61 depositCurrency = wallet.currency,
62 providerList = [ChangeNowExchangeProvider(), SideShiftExchangeProvider(), SimpleSwapExchangeProvider()],
62 - selectedProviders = ObservableList<ExchangeProvider>(),
63 - currentTradeAvailableProviders = SplayTreeMap<double, ExchangeProvider>() {
63 + selectedProviders = ObservableList<ExchangeProvider>() {
64 const excludeDepositCurrencies = [CryptoCurrency.btt, CryptoCurrency.nano];
65 const excludeReceiveCurrencies = [CryptoCurrency.xlm, CryptoCurrency.xrp,
66 CryptoCurrency.bnb, CryptoCurrency.btt, CryptoCurrency.nano];
67 _initialPairBasedOnWallet();
68 +
69 final Map<String, dynamic> exchangeProvidersSelection = json
70 .decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map<String, dynamic>;
71
@@ -76,6 +77,11 @@ abstract class ExchangeViewModelBase with Store {
77 : (exchangeProvidersSelection[element.title] as bool))
78 .toList());
79
80 + _setAvailableProviders();
81 + _calculateBestRate();
82 +
83 + bestRateSync = Timer.periodic(Duration(seconds: 10), (timer) => _calculateBestRate());
84 +
85 isDepositAddressEnabled = !(depositCurrency == wallet.currency);
86 isReceiveAddressEnabled = !(receiveCurrency == wallet.currency);
87 depositAmount = '';
@@ -119,8 +125,15 @@ abstract class ExchangeViewModelBase with Store {
125 /// Maps in dart are not sorted by default
126 /// SplayTreeMap is a map sorted by keys
127 /// will use it to sort available providers
122 - /// depending on the amount they yield for the current trade
123 - SplayTreeMap<double, ExchangeProvider> currentTradeAvailableProviders;
128 + /// based on the rate they yield for the current trade
129 + ///
130 + ///
131 + /// initialize with descending comparator
132 + /// since we want largest rate first
133 + final SplayTreeMap<double, ExchangeProvider> _sortedAvailableProviders =
134 + SplayTreeMap<double, ExchangeProvider>((double a, double b) => b.compareTo(a));
135 +
136 + final List<ExchangeProvider> _tradeAvailableProviders = [];
137
138 @observable
139 ObservableList<ExchangeProvider> selectedProviders;
@@ -191,6 +204,10 @@ abstract class ExchangeViewModelBase with Store {
204
205 final SettingsStore _settingsStore;
206
207 + double _bestRate = 0.0;
208 +
209 + late Timer bestRateSync;
210 +
211 @action
212 void changeDepositCurrency({required CryptoCurrency currency}) {
213 depositCurrency = currency;
@@ -210,68 +227,36 @@ abstract class ExchangeViewModelBase with Store {
227 }
228
229 @action
213 - void changeReceiveAmount({required String amount}) {
230 + Future<void> changeReceiveAmount({required String amount}) async {
231 receiveAmount = amount;
232 isReverse = true;
233
217 - if (amount == null || amount.isEmpty) {
234 + if (amount.isEmpty) {
235 depositAmount = '';
236 receiveAmount = '';
237 return;
238 }
239
223 - final _enteredAmount = double.parse(amount.replaceAll(',', '.')) ?? 0;
240 + final _enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0;
241
225 - currentTradeAvailableProviders.clear();
226 - for (var provider in selectedProviders) {
227 - /// if this provider is not valid for the current pair, skip it
228 - if (!providersForCurrentPair().contains(provider)) {
229 - continue;
230 - }
231 - provider
232 - .calculateAmount(
233 - from: receiveCurrency,
234 - to: depositCurrency,
235 - amount: _enteredAmount,
236 - isFixedRateMode: isFixedRateMode,
237 - isReceiveAmount: true)
238 - .then((amount) {
239 -
240 - final from = isFixedRateMode
241 - ? receiveCurrency
242 - : depositCurrency;
243 - final to = isFixedRateMode
244 - ? depositCurrency
245 - : receiveCurrency;
246 -
247 - provider.fetchLimits(
248 - from: from,
249 - to: to,
250 - isFixedRateMode: isFixedRateMode,
251 - ).then((limits) {
252 - /// if the entered amount doesn't exceed the limits of this provider
253 - if ((limits?.max ?? double.maxFinite) >= _enteredAmount
254 - && (limits?.min ?? 0) <= _enteredAmount) {
255 - /// add this provider as its valid for this trade
256 - /// will be sorted ascending already since
257 - /// we seek the least deposit amount
258 - currentTradeAvailableProviders[amount] = provider;
259 - }
260 - return amount;
261 - }).then((amount) => depositAmount = _cryptoNumberFormat
262 - .format(amount)
263 - .toString()
264 - .replaceAll(RegExp('\\,'), ''));
265 - });
242 + if (_bestRate == 0) {
243 + depositAmount = S.current.fetching;
244 +
245 + await _calculateBestRate();
246 }
247 +
248 + depositAmount = _cryptoNumberFormat
249 + .format(_enteredAmount / _bestRate)
250 + .toString()
251 + .replaceAll(RegExp('\\,'), '');
252 }
253
254 @action
270 - void changeDepositAmount({required String amount}) {
255 + Future<void> changeDepositAmount({required String amount}) async {
256 depositAmount = amount;
257 isReverse = false;
258
274 - if (amount == null || amount.isEmpty) {
259 + if (amount.isEmpty) {
260 depositAmount = '';
261 receiveAmount = '';
262 return;
@@ -279,93 +264,90 @@ abstract class ExchangeViewModelBase with Store {
264
265 final _enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0;
266
282 - currentTradeAvailableProviders.clear();
283 - for (var provider in selectedProviders) {
284 - /// if this provider is not valid for the current pair, skip it
285 - if (!providersForCurrentPair().contains(provider)) {
286 - continue;
267 + /// in case the best rate was not calculated yet
268 + if (_bestRate == 0) {
269 + receiveAmount = S.current.fetching;
270 +
271 + await _calculateBestRate();
272 + }
273 +
274 + receiveAmount = _cryptoNumberFormat
275 + .format(_bestRate * _enteredAmount)
276 + .toString()
277 + .replaceAll(RegExp('\\,'), '');
278 + }
279 +
280 + Future<void> _calculateBestRate() async {
281 + final result = await Future.wait<double>(
282 + _tradeAvailableProviders
283 + .map((element) => element.calculateAmount(
284 + from: depositCurrency,
285 + to: receiveCurrency,
286 + amount: 1,
287 + isFixedRateMode: isFixedRateMode,
288 + isReceiveAmount: false))
289 + );
290 +
291 + _sortedAvailableProviders.clear();
292 +
293 + for (int i=0;i<result.length;i++) {
294 + if (result[i] != 0) {
295 + /// add this provider as its valid for this trade
296 + _sortedAvailableProviders[result[i]] = _tradeAvailableProviders[i];
297 }
288 - provider
289 - .calculateAmount(
290 - from: depositCurrency,
291 - to: receiveCurrency,
292 - amount: _enteredAmount,
293 - isFixedRateMode: isFixedRateMode,
294 - isReceiveAmount: false)
295 - .then((amount) {
296 -
297 - final from = isFixedRateMode
298 - ? receiveCurrency
299 - : depositCurrency;
300 - final to = isFixedRateMode
301 - ? depositCurrency
302 - : receiveCurrency;
303 -
304 - provider.fetchLimits(
305 - from: from,
306 - to: to,
307 - isFixedRateMode: isFixedRateMode,
308 - ).then((limits) {
309 -
310 - /// if the entered amount doesn't exceed the limits of this provider
311 - if ((limits?.max ?? double.maxFinite) >= _enteredAmount
312 - && (limits?.min ?? 0) <= _enteredAmount) {
313 - /// add this provider as its valid for this trade
314 - /// subtract from maxFinite so the provider
315 - /// with the largest amount would be sorted ascending
316 - currentTradeAvailableProviders[double.maxFinite - amount] = provider;
317 - }
318 - return amount;
319 - }).then((amount) => receiveAmount =
320 - receiveAmount = _cryptoNumberFormat
321 - .format(amount)
322 - .toString()
323 - .replaceAll(RegExp('\\,'), ''));
324 - });
298 + }
299 + if (_sortedAvailableProviders.isNotEmpty) {
300 + _bestRate = _sortedAvailableProviders.keys.first;
301 }
302 }
303
304 @action
329 - Future loadLimits() async {
305 + Future<void> loadLimits() async {
306 if (selectedProviders.isEmpty) {
307 return;
308 }
309
310 limitsState = LimitsIsLoading();
311
336 - try {
337 - final from = isFixedRateMode
312 + final from = isFixedRateMode
313 ? receiveCurrency
314 : depositCurrency;
340 - final to = isFixedRateMode
315 + final to = isFixedRateMode
316 ? depositCurrency
317 : receiveCurrency;
318
344 - limits = await selectedProviders.first.fetchLimits(
345 - from: from,
346 - to: to,
347 - isFixedRateMode: isFixedRateMode);
348 -
349 - /// if the first provider limits is bounded then check with other providers
350 - /// for the highest maximum limit
351 - if (limits.max != null) {
352 - for (int i = 1;i < selectedProviders.length;i++) {
353 - final Limits tempLimits = await selectedProviders[i].fetchLimits(
354 - from: from,
355 - to: to,
356 - isFixedRateMode: isFixedRateMode);
357 -
358 - /// set the limits with the maximum provider limit
359 - /// if there is a provider with null max then it's the maximum limit
360 - if ((tempLimits.max ?? double.maxFinite) > limits.max!) {
361 - limits = tempLimits;
362 - }
319 + double lowestMin = double.maxFinite;
320 + double? highestMax = 0.0;
321 +
322 + for (var provider in selectedProviders) {
323 + /// if this provider is not valid for the current pair, skip it
324 + if (!providersForCurrentPair().contains(provider)) {
325 + continue;
326 + }
327 +
328 + try {
329 + final tempLimits = await provider.fetchLimits(
330 + from: from,
331 + to: to,
332 + isFixedRateMode: isFixedRateMode);
333 +
334 + if (tempLimits.min != null && tempLimits.min! < lowestMin) {
335 + lowestMin = tempLimits.min!;
336 + }
337 + if (highestMax != null && (tempLimits.max ?? double.maxFinite) > highestMax) {
338 + highestMax = tempLimits.max;
339 }
340 + } catch (e) {
341 + continue;
342 }
343 + }
344 +
345 + if (lowestMin < double.maxFinite) {
346 + limits = Limits(min: lowestMin, max: highestMax);
347
348 limitsState = LimitsLoadedSuccessfully(limits: limits);
367 - } catch (e) {
368 - limitsState = LimitsLoadedFailure(error: e.toString());
349 + } else {
350 + limitsState = LimitsLoadedFailure(error: 'Limits loading failed');
351 }
352 }
353
@@ -374,7 +356,7 @@ abstract class ExchangeViewModelBase with Store {
356 TradeRequest? request;
357 String amount = '';
358
377 - for (var provider in currentTradeAvailableProviders.values) {
359 + for (var provider in _sortedAvailableProviders.values) {
360 if (!(await provider.checkIsAvailable())) {
361 continue;
362 }
@@ -383,7 +365,7 @@ abstract class ExchangeViewModelBase with Store {
365 request = SideShiftRequest(
366 depositMethod: depositCurrency,
367 settleMethod: receiveCurrency,
386 - depositAmount: depositAmount?.replaceAll(',', '.') ?? '',
368 + depositAmount: depositAmount.replaceAll(',', '.'),
369 settleAddress: receiveAddress,
370 refundAddress: depositAddress,
371 );
@@ -394,7 +376,7 @@ abstract class ExchangeViewModelBase with Store {
376 request = SimpleSwapRequest(
377 from: depositCurrency,
378 to: receiveCurrency,
397 - amount: depositAmount?.replaceAll(',', '.') ?? '',
379 + amount: depositAmount.replaceAll(',', '.'),
380 address: receiveAddress,
381 refundAddress: depositAddress,
382 );
@@ -405,8 +387,8 @@ abstract class ExchangeViewModelBase with Store {
387 request = XMRTOTradeRequest(
388 from: depositCurrency,
389 to: receiveCurrency,
408 - amount: depositAmount?.replaceAll(',', '.') ?? '',
409 - receiveAmount: receiveAmount?.replaceAll(',', '.') ?? '',
390 + amount: depositAmount.replaceAll(',', '.'),
391 + receiveAmount: receiveAmount.replaceAll(',', '.'),
392 address: receiveAddress,
393 refundAddress: depositAddress,
394 isBTCRequest: isReceiveAmountEntered);
@@ -417,8 +399,8 @@ abstract class ExchangeViewModelBase with Store {
399 request = ChangeNowRequest(
400 from: depositCurrency,
401 to: receiveCurrency,
420 - fromAmount: depositAmount?.replaceAll(',', '.') ?? '',
421 - toAmount: receiveAmount?.replaceAll(',', '.') ?? '',
402 + fromAmount: depositAmount.replaceAll(',', '.'),
403 + toAmount: receiveAmount.replaceAll(',', '.'),
404 refundAddress: depositAddress,
405 address: receiveAddress,
406 isReverse: isReverse);
@@ -429,7 +411,7 @@ abstract class ExchangeViewModelBase with Store {
411 request = MorphTokenRequest(
412 from: depositCurrency,
413 to: receiveCurrency,
432 - amount: depositAmount?.replaceAll(',', '.') ?? '',
414 + amount: depositAmount.replaceAll(',', '.'),
415 refundAddress: depositAddress,
416 address: receiveAddress);
417 amount = depositAmount;
@@ -437,7 +419,7 @@ abstract class ExchangeViewModelBase with Store {
419
420 amount = amount.replaceAll(',', '.');
421
440 - if (limitsState is LimitsLoadedSuccessfully && amount != null) {
422 + if (limitsState is LimitsLoadedSuccessfully) {
423 if (double.parse(amount) < limits.min!) {
424 continue;
425 } else if (limits.max != null && double.parse(amount) > limits.max!) {
@@ -527,7 +509,7 @@ abstract class ExchangeViewModelBase with Store {
509 final providers = providerList
510 .where((provider) => provider.pairList
511 .where((pair) =>
530 - pair.from == (from ?? depositCurrency) && pair.to == (to ?? receiveCurrency))
512 + pair.from == from && pair.to == to)
513 .isNotEmpty)
514 .toList();
515
@@ -537,6 +519,10 @@ abstract class ExchangeViewModelBase with Store {
519 void _onPairChange() {
520 depositAmount = '';
521 receiveAmount = '';
522 + loadLimits();
523 + _setAvailableProviders();
524 + _bestRate = 0;
525 + _calculateBestRate();
526 }
527
528 void _initialPairBasedOnWallet() {
@@ -579,11 +565,15 @@ abstract class ExchangeViewModelBase with Store {
565 @action
566 void addExchangeProvider(ExchangeProvider provider) {
567 selectedProviders.add(provider);
568 + if (providersForCurrentPair().contains(provider)) {
569 + _tradeAvailableProviders.add(provider);
570 + }
571 }
572
573 @action
574 void removeExchangeProvider(ExchangeProvider provider) {
575 selectedProviders.remove(provider);
576 + _tradeAvailableProviders.remove(provider);
577 }
578
579 @action
@@ -593,13 +583,14 @@ abstract class ExchangeViewModelBase with Store {
583 isFixedRateMode = false;
584 _defineIsReceiveAmountEditable();
585 loadLimits();
586 + _bestRate = 0;
587 + _calculateBestRate();
588
589 final Map<String, dynamic> exchangeProvidersSelection = json
590 .decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map<String, dynamic>;
591
600 - exchangeProvidersSelection.updateAll((key, dynamic value) => false);
601 - for (var provider in selectedProviders) {
602 - exchangeProvidersSelection[provider.title] = true;
592 + for (var provider in providerList) {
593 + exchangeProvidersSelection[provider.title] = selectedProviders.contains(provider);
594 }
595
596 sharedPreferences.setString(
@@ -612,4 +603,12 @@ abstract class ExchangeViewModelBase with Store {
603 final providersForPair = providersForCurrentPair();
604 return selectedProviders.any((element) => element.isAvailable && providersForPair.contains(element));
605 }
606 +
607 + void _setAvailableProviders() {
608 + _tradeAvailableProviders.clear();
609 +
610 + _tradeAvailableProviders.addAll(
611 + selectedProviders
612 + .where((provider) => providersForCurrentPair().contains(provider)));
613 + }
614 }