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
}