1
+import 'dart:collection';
2
+import 'dart:convert';
3
+
4
+import 'package:cake_wallet/entities/preferences_key.dart';
5
import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart';
6
import 'package:cake_wallet/exchange/sideshift/sideshift_request.dart';
7
import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart';
31
import 'package:cake_wallet/exchange/morphtoken/morphtoken_request.dart';
32
import 'package:cake_wallet/store/templates/exchange_template_store.dart';
33
import 'package:cake_wallet/exchange/exchange_template.dart';
34
+import 'package:shared_preferences/shared_preferences.dart';
35
36
part 'exchange_view_model.g.dart';
37
39
40
abstract class ExchangeViewModelBase with Store {
41
ExchangeViewModelBase(this.wallet, this.trades, this._exchangeTemplateStore,
37
- this.tradesStore, this._settingsStore) {
42
+ this.tradesStore, this._settingsStore, this.sharedPreferences) {
43
const excludeDepositCurrencies = [CryptoCurrency.xhv];
44
const excludeReceiveCurrencies = [CryptoCurrency.xlm, CryptoCurrency.xrp, CryptoCurrency.bnb, CryptoCurrency.xhv];
45
providerList = [ChangeNowExchangeProvider(), SideShiftExchangeProvider(), SimpleSwapExchangeProvider()];
46
+
47
+ currentTradeAvailableProviders = SplayTreeMap<double, ExchangeProvider>();
48
+
49
+ final Map<String, dynamic> exchangeProvidersSelection = json
50
+ .decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map<String, dynamic>;
51
+
52
+ /// if the provider is not in the user settings (user's first time or newly added provider)
53
+ /// then use its default value decided by us
54
+ selectedProviders = ObservableList.of(providerList.where(
55
+ (element) => exchangeProvidersSelection[element.title] == null
56
+ ? element.isEnabled
57
+ : (exchangeProvidersSelection[element.title] as bool))
58
+ .toList());
59
+
60
_initialPairBasedOnWallet();
61
isDepositAddressEnabled = !(depositCurrency == wallet.currency);
62
isReceiveAddressEnabled = !(receiveCurrency == wallet.currency);
67
? wallet.walletAddresses.address : '';
68
limitsState = LimitsInitialState();
69
tradeState = ExchangeTradeStateInitial();
51
- _cryptoNumberFormat = NumberFormat()..maximumFractionDigits = 12;
70
+ _cryptoNumberFormat = NumberFormat()..maximumFractionDigits = wallet.type == WalletType.bitcoin ? 8 : 12;
71
provider = providersForCurrentPair().first;
72
final initialProvider = provider;
73
provider.checkIsAvailable().then((bool isAvailable) {
98
final Box<Trade> trades;
99
final ExchangeTemplateStore _exchangeTemplateStore;
100
final TradesStore tradesStore;
101
+ final SharedPreferences sharedPreferences;
102
103
@observable
104
ExchangeProvider provider;
105
106
+ /// Maps in dart are not sorted by default
107
+ /// SplayTreeMap is a map sorted by keys
108
+ /// will use it to sort available providers
109
+ /// depending on the amount they yield for the current trade
110
+ SplayTreeMap<double, ExchangeProvider> currentTradeAvailableProviders;
111
+
112
+ @observable
113
+ ObservableList<ExchangeProvider> selectedProviders;
114
+
115
@observable
116
List<ExchangeProvider> providerList;
117
176
177
NumberFormat _cryptoNumberFormat;
178
150
- SettingsStore _settingsStore;
151
-
152
- @action
153
- void changeProvider({ExchangeProvider provider}) {
154
- this.provider = provider;
155
- depositAmount = '';
156
- receiveAmount = '';
157
- isFixedRateMode = false;
158
- _defineIsReceiveAmountEditable();
159
- loadLimits();
160
- }
179
+ final SettingsStore _settingsStore;
180
181
@action
182
void changeDepositCurrency({CryptoCurrency currency}) {
207
return;
208
}
209
191
- final _amount = double.parse(amount.replaceAll(',', '.')) ?? 0;
192
-
193
- provider
194
- .calculateAmount(
195
- from: receiveCurrency,
196
- to: depositCurrency,
197
- amount: _amount,
198
- isFixedRateMode: isFixedRateMode,
199
- isReceiveAmount: true)
200
- .then((amount) => _cryptoNumberFormat
210
+ final _enteredAmount = double.parse(amount.replaceAll(',', '.')) ?? 0;
211
+
212
+ currentTradeAvailableProviders.clear();
213
+ for (var provider in selectedProviders) {
214
+ provider
215
+ .calculateAmount(
216
+ from: receiveCurrency,
217
+ to: depositCurrency,
218
+ amount: _enteredAmount,
219
+ isFixedRateMode: isFixedRateMode,
220
+ isReceiveAmount: true)
221
+ .then((amount) {
222
+
223
+ final from = isFixedRateMode
224
+ ? receiveCurrency
225
+ : depositCurrency;
226
+ final to = isFixedRateMode
227
+ ? depositCurrency
228
+ : receiveCurrency;
229
+
230
+ provider.fetchLimits(
231
+ from: from,
232
+ to: to,
233
+ isFixedRateMode: isFixedRateMode,
234
+ ).then((limits) {
235
+ /// if the entered amount doesn't exceed the limits of this provider
236
+ if ((limits.max ?? double.maxFinite) >= _enteredAmount
237
+ && (limits.min ?? 0) <= _enteredAmount) {
238
+ /// add this provider as its valid for this trade
239
+ /// will be sorted ascending already since
240
+ /// we seek the least deposit amount
241
+ currentTradeAvailableProviders[amount] = provider;
242
+ }
243
+ return amount;
244
+ }).then((amount) => depositAmount = _cryptoNumberFormat
245
.format(amount)
246
.toString()
203
- .replaceAll(RegExp('\\,'), ''))
204
- .then((amount) => depositAmount = amount);
247
+ .replaceAll(RegExp('\\,'), ''));
248
+ });
249
+ }
250
}
251
252
@action
260
return;
261
}
262
218
- final _amount = double.parse(amount.replaceAll(',', '.')) ?? 0;
219
- provider
220
- .calculateAmount(
221
- from: depositCurrency,
222
- to: receiveCurrency,
223
- amount: _amount,
224
- isFixedRateMode: isFixedRateMode,
225
- isReceiveAmount: false)
226
- .then((amount) => _cryptoNumberFormat
263
+ final _enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0;
264
+
265
+ currentTradeAvailableProviders.clear();
266
+ for (var provider in selectedProviders) {
267
+ provider
268
+ .calculateAmount(
269
+ from: depositCurrency,
270
+ to: receiveCurrency,
271
+ amount: _enteredAmount,
272
+ isFixedRateMode: isFixedRateMode,
273
+ isReceiveAmount: false)
274
+ .then((amount) {
275
+
276
+ final from = isFixedRateMode
277
+ ? receiveCurrency
278
+ : depositCurrency;
279
+ final to = isFixedRateMode
280
+ ? depositCurrency
281
+ : receiveCurrency;
282
+
283
+ provider.fetchLimits(
284
+ from: from,
285
+ to: to,
286
+ isFixedRateMode: isFixedRateMode,
287
+ ).then((limits) {
288
+
289
+ /// if the entered amount doesn't exceed the limits of this provider
290
+ if ((limits.max ?? double.maxFinite) >= _enteredAmount
291
+ && (limits.min ?? 0) <= _enteredAmount) {
292
+ /// add this provider as its valid for this trade
293
+ /// subtract from maxFinite so the provider
294
+ /// with the largest amount would be sorted ascending
295
+ currentTradeAvailableProviders[double.maxFinite - amount] = provider;
296
+ }
297
+ return amount;
298
+ }).then((amount) => receiveAmount =
299
+ receiveAmount = _cryptoNumberFormat
300
.format(amount)
301
.toString()
229
- .replaceAll(RegExp('\\,'), ''))
230
- .then((amount) => receiveAmount = amount);
302
+ .replaceAll(RegExp('\\,'), ''));
303
+ });
304
+ }
305
}
306
307
@action
308
Future loadLimits() async {
309
+ if (selectedProviders.isEmpty) {
310
+ return;
311
+ }
312
+
313
limitsState = LimitsIsLoading();
314
315
try {
319
final to = isFixedRateMode
320
? depositCurrency
321
: receiveCurrency;
244
- limits = await provider.fetchLimits(
322
+
323
+ limits = await selectedProviders.first.fetchLimits(
324
from: from,
325
to: to,
326
isFixedRateMode: isFixedRateMode);
327
+
328
+ /// if the first provider limits is bounded then check with other providers
329
+ /// for the highest maximum limit
330
+ if (limits.max != null) {
331
+ for (int i = 1;i < selectedProviders.length;i++) {
332
+ final Limits tempLimits = await selectedProviders[i].fetchLimits(
333
+ from: from,
334
+ to: to,
335
+ isFixedRateMode: isFixedRateMode);
336
+
337
+ /// set the limits with the maximum provider limit
338
+ /// if there is a provider with null max then it's the maximum limit
339
+ if ((tempLimits.max ?? double.maxFinite) > limits.max) {
340
+ limits = tempLimits;
341
+ }
342
+ }
343
+ }
344
+
345
limitsState = LimitsLoadedSuccessfully(limits: limits);
346
} catch (e) {
347
limitsState = LimitsLoadedFailure(error: e.toString());
352
Future createTrade() async {
353
TradeRequest request;
354
String amount;
258
- CryptoCurrency currency;
355
260
- if (provider is SideShiftExchangeProvider) {
261
- request = SideShiftRequest(
356
+ for (var provider in currentTradeAvailableProviders.values) {
357
+ if (!(await provider.checkIsAvailable())) {
358
+ continue;
359
+ }
360
+
361
+ if (provider is SideShiftExchangeProvider) {
362
+ request = SideShiftRequest(
363
depositMethod: depositCurrency,
364
settleMethod: receiveCurrency,
365
depositAmount: depositAmount?.replaceAll(',', '.'),
366
settleAddress: receiveAddress,
367
refundAddress: depositAddress,
267
- );
268
- amount = depositAmount;
269
- currency = depositCurrency;
270
- }
368
+ );
369
+ amount = depositAmount;
370
+ }
371
272
- if (provider is SimpleSwapExchangeProvider) {
273
- request = SimpleSwapRequest(
372
+ if (provider is SimpleSwapExchangeProvider) {
373
+ request = SimpleSwapRequest(
374
from: depositCurrency,
375
to: receiveCurrency,
376
amount: depositAmount?.replaceAll(',', '.'),
377
address: receiveAddress,
378
refundAddress: depositAddress,
279
- );
280
- amount = depositAmount;
281
- currency = depositCurrency;
282
- }
379
+ );
380
+ amount = depositAmount;
381
+ }
382
284
- if (provider is XMRTOExchangeProvider) {
285
- request = XMRTOTradeRequest(
286
- from: depositCurrency,
287
- to: receiveCurrency,
288
- amount: depositAmount?.replaceAll(',', '.'),
289
- receiveAmount: receiveAmount?.replaceAll(',', '.'),
290
- address: receiveAddress,
291
- refundAddress: depositAddress,
292
- isBTCRequest: isReceiveAmountEntered);
293
- amount = depositAmount;
294
- currency = depositCurrency;
295
- }
383
+ if (provider is XMRTOExchangeProvider) {
384
+ request = XMRTOTradeRequest(
385
+ from: depositCurrency,
386
+ to: receiveCurrency,
387
+ amount: depositAmount?.replaceAll(',', '.'),
388
+ receiveAmount: receiveAmount?.replaceAll(',', '.'),
389
+ address: receiveAddress,
390
+ refundAddress: depositAddress,
391
+ isBTCRequest: isReceiveAmountEntered);
392
+ amount = depositAmount;
393
+ }
394
297
- if (provider is ChangeNowExchangeProvider) {
298
- request = ChangeNowRequest(
299
- from: depositCurrency,
300
- to: receiveCurrency,
301
- fromAmount: depositAmount?.replaceAll(',', '.'),
302
- toAmount: receiveAmount?.replaceAll(',', '.'),
303
- refundAddress: depositAddress,
304
- address: receiveAddress,
305
- isReverse: isReverse);
306
- amount = isReverse ? receiveAmount : depositAmount;
307
- currency = depositCurrency;
308
- }
395
+ if (provider is ChangeNowExchangeProvider) {
396
+ request = ChangeNowRequest(
397
+ from: depositCurrency,
398
+ to: receiveCurrency,
399
+ fromAmount: depositAmount?.replaceAll(',', '.'),
400
+ toAmount: receiveAmount?.replaceAll(',', '.'),
401
+ refundAddress: depositAddress,
402
+ address: receiveAddress,
403
+ isReverse: isReverse);
404
+ amount = isReverse ? receiveAmount : depositAmount;
405
+ }
406
310
- if (provider is MorphTokenExchangeProvider) {
311
- request = MorphTokenRequest(
312
- from: depositCurrency,
313
- to: receiveCurrency,
314
- amount: depositAmount?.replaceAll(',', '.'),
315
- refundAddress: depositAddress,
316
- address: receiveAddress);
317
- amount = depositAmount;
318
- currency = depositCurrency;
319
- }
407
+ if (provider is MorphTokenExchangeProvider) {
408
+ request = MorphTokenRequest(
409
+ from: depositCurrency,
410
+ to: receiveCurrency,
411
+ amount: depositAmount?.replaceAll(',', '.'),
412
+ refundAddress: depositAddress,
413
+ address: receiveAddress);
414
+ amount = depositAmount;
415
+ }
416
321
- amount = amount.replaceAll(',', '.');
322
-
323
- if (limitsState is LimitsLoadedSuccessfully && amount != null) {
324
- if (double.parse(amount) < limits.min) {
325
- tradeState = TradeIsCreatedFailure(
326
- title: provider.title,
327
- error: S.current.error_text_minimal_limit('${provider.description}',
328
- '${limits.min}', currency.toString()));
329
- } else if (limits.max != null && double.parse(amount) > limits.max) {
330
- tradeState = TradeIsCreatedFailure(
331
- title: provider.title,
332
- error: S.current.error_text_maximum_limit('${provider.description}',
333
- '${limits.max}', currency.toString()));
334
- } else {
335
- try {
336
- tradeState = TradeIsCreating();
337
- final trade = await provider.createTrade(
338
- request: request, isFixedRateMode: isFixedRateMode);
339
- trade.walletId = wallet.id;
340
- tradesStore.setTrade(trade);
341
- await trades.add(trade);
342
- tradeState = TradeIsCreatedSuccessfully(trade: trade);
343
- } catch (e) {
344
- tradeState =
345
- TradeIsCreatedFailure(title: provider.title, error: e.toString());
417
+ amount = amount.replaceAll(',', '.');
418
+
419
+ if (limitsState is LimitsLoadedSuccessfully && amount != null) {
420
+ if (double.parse(amount) < limits.min) {
421
+ continue;
422
+ } else if (limits.max != null && double.parse(amount) > limits.max) {
423
+ continue;
424
+ } else {
425
+ try {
426
+ tradeState = TradeIsCreating();
427
+ final trade = await provider.createTrade(
428
+ request: request, isFixedRateMode: isFixedRateMode);
429
+ trade.walletId = wallet.id;
430
+ tradesStore.setTrade(trade);
431
+ await trades.add(trade);
432
+ tradeState = TradeIsCreatedSuccessfully(trade: trade);
433
+ /// return after the first successful trade
434
+ return;
435
+ } catch (e) {
436
+ continue;
437
+ }
438
}
439
}
348
- } else {
349
- tradeState = TradeIsCreatedFailure(
350
- title: provider.title,
351
- error: S.current
352
- .error_text_limits_loading_failed('${provider.description}'));
440
}
441
+
442
+ /// if the code reached here then none of the providers succeeded
443
+ tradeState = TradeIsCreatedFailure(
444
+ title: S.current.trade_not_created,
445
+ error: S.current.none_of_selected_providers_can_exchange);
446
}
447
448
@action
506
final providers = providerList
507
.where((provider) => provider.pairList
508
.where((pair) =>
417
- pair.from == depositCurrency && pair.to == receiveCurrency)
509
+ pair.from == (from ?? depositCurrency) && pair.to == (to ?? receiveCurrency))
510
.isNotEmpty)
511
.toList();
512
514
}
515
516
void _onPairChange() {
425
- final isPairExist = provider.pairList
426
- .where((pair) =>
427
- pair.from == depositCurrency && pair.to == receiveCurrency)
428
- .isNotEmpty;
429
-
430
- if (isPairExist) {
431
- final provider =
432
- _providerForPair(from: depositCurrency, to: receiveCurrency);
433
-
434
- if (provider != null) {
435
- changeProvider(provider: provider);
436
- }
437
- } else {
438
- depositAmount = '';
439
- receiveAmount = '';
440
- }
441
- }
442
-
443
- ExchangeProvider _providerForPair({CryptoCurrency from, CryptoCurrency to}) {
444
- final providers = _providersForPair(from: from, to: to);
445
- return providers.isNotEmpty ? providers[0] : null;
517
+ depositAmount = '';
518
+ receiveAmount = '';
519
}
520
521
void _initialPairBasedOnWallet() {
546
isReceiveAmountEditable = false;
547
}*/
548
//isReceiveAmountEditable = false;
476
- isReceiveAmountEditable = provider is ChangeNowExchangeProvider || provider is SimpleSwapExchangeProvider;
549
+ // isReceiveAmountEditable = selectedProviders.any((provider) => provider is ChangeNowExchangeProvider);
550
+ // isReceiveAmountEditable = provider is ChangeNowExchangeProvider || provider is SimpleSwapExchangeProvider;
551
+ isReceiveAmountEditable = true;
552
+ }
553
+
554
+ @action
555
+ void addExchangeProvider(ExchangeProvider provider) {
556
+ selectedProviders.add(provider);
557
+ }
558
+
559
+ @action
560
+ void removeExchangeProvider(ExchangeProvider provider) {
561
+ selectedProviders.remove(provider);
562
+ }
563
+
564
+ @action
565
+ void saveSelectedProviders() {
566
+ depositAmount = '';
567
+ receiveAmount = '';
568
+ isFixedRateMode = false;
569
+ _defineIsReceiveAmountEditable();
570
+ loadLimits();
571
+
572
+ final Map<String, dynamic> exchangeProvidersSelection = json
573
+ .decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map<String, dynamic>;
574
+
575
+ exchangeProvidersSelection.updateAll((key, dynamic value) => false);
576
+ for (var provider in selectedProviders) {
577
+ exchangeProvidersSelection[provider.title] = true;
578
+ }
579
+
580
+ sharedPreferences.setString(
581
+ PreferencesKey.exchangeProvidersSelection,
582
+ json.encode(exchangeProvidersSelection),
583
+ );
584
+ }
585
+
586
+ bool get isAvailableInSelected {
587
+ final providersForPair = providersForCurrentPair();
588
+ return selectedProviders.any((element) => element.isAvailable && providersForPair.contains(element));
589
}
590
}