dev
dart 247 lines 8.9 KB
Raw
1 import 'dart:async';
2 import 'package:cake_wallet/exchange/provider/jupiter_exchange_provider.dart';
3 import 'package:cake_wallet/exchange/provider/near_Intents_exchange_provider.dart';
4 import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart';
5 import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
6 import 'package:cake_wallet/exchange/trade.dart';
7 import 'package:cake_wallet/exchange/trade_state.dart';
8 import 'package:cake_wallet/store/dashboard/trades_store.dart';
9 import 'package:cake_wallet/entities/exchange_api_mode.dart';
10 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
11 import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
12 import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
13 import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
14 import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
15 import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart';
16 import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart';
17 import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
18 import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
19 import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
20 import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
21 import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart';
22 import 'package:cw_core/utils/print_verbose.dart';
23 import 'package:cake_wallet/store/app_store.dart';
24 import 'package:shared_preferences/shared_preferences.dart';
25
26 class TradeMonitor {
27 static const int _tradeCheckIntervalMinutes = 5;
28 static const int _maxTradeAgeHours = 24;
29
30 TradeMonitor({
31 required this.tradesStore,
32 required this.appStore,
33 required this.preferences,
34 });
35
36 final TradesStore tradesStore;
37 final AppStore appStore;
38 final Map<String, Timer> _tradeTimers = {};
39 final SharedPreferences preferences;
40
41 ExchangeProvider? _getProviderByDescription(ExchangeProviderDescription description) {
42 switch (description) {
43 case ExchangeProviderDescription.changeNow:
44 return ChangeNowExchangeProvider(settingsStore: appStore.settingsStore);
45 case ExchangeProviderDescription.sideShift:
46 return SideShiftExchangeProvider();
47 case ExchangeProviderDescription.simpleSwap:
48 return SimpleSwapExchangeProvider();
49 case ExchangeProviderDescription.trocador:
50 return TrocadorExchangeProvider();
51 case ExchangeProviderDescription.exolix:
52 return ExolixExchangeProvider();
53 case ExchangeProviderDescription.thorChain:
54 return ThorChainExchangeProvider();
55 case ExchangeProviderDescription.swapTrade:
56 return SwapTradeExchangeProvider();
57 case ExchangeProviderDescription.letsExchange:
58 return LetsExchangeExchangeProvider();
59 case ExchangeProviderDescription.stealthEx:
60 return StealthExExchangeProvider();
61 case ExchangeProviderDescription.chainflip:
62 return ChainflipExchangeProvider();
63 case ExchangeProviderDescription.xoSwap:
64 return XOSwapExchangeProvider();
65 case ExchangeProviderDescription.swapsXyz:
66 return SwapsXyzExchangeProvider();
67 case ExchangeProviderDescription.jupiter:
68 return JupiterExchangeProvider();
69 case ExchangeProviderDescription.nearIntents:
70 return NearIntentsExchangeProvider();
71 }
72 return null;
73 }
74
75 void monitorActiveTrades(String walletId) {
76 // Checks if the trade monitoring is permitted
77 // i.e the user has not disabled the exchange api mode or the status updates
78 final isTradeMonitoringPermitted = _isTradeMonitoringPermitted();
79 if (!isTradeMonitoringPermitted) {
80 return;
81 }
82
83 final trades = tradesStore.trades;
84 final tradesToCancel = <String>[];
85
86 for (final item in trades) {
87 final trade = item.trade;
88
89 final provider = _getProviderByDescription(trade.provider);
90
91 // Multiple checks to see if to skip the trade, if yes, we cancel the timer if it exists
92 if (_shouldSkipTrade(trade, walletId, provider)) {
93 tradesToCancel.add(trade.id);
94 continue;
95 }
96
97 if (_tradeTimers.containsKey(trade.id)) {
98 continue;
99 } else {
100 _startTradeMonitoring(trade, provider!);
101 }
102 }
103
104 // After going through the list of available trades, we cancel the timers in the tradesToCancel list
105 _cancelMultipleTradeTimers(tradesToCancel);
106 }
107
108 bool _isTradeMonitoringPermitted() {
109 final disableAutomaticExchangeStatusUpdates =
110 appStore.settingsStore.disableAutomaticExchangeStatusUpdates;
111 if (disableAutomaticExchangeStatusUpdates) {
112 printV('Automatic exchange status updates are disabled');
113 return false;
114 }
115
116 final exchangeApiMode = appStore.settingsStore.exchangeStatus;
117 if (exchangeApiMode == ExchangeApiMode.disabled) {
118 printV('Exchange API mode is disabled');
119 return false;
120 }
121
122 return true;
123 }
124
125 bool _shouldSkipTrade(Trade trade, String walletId, ExchangeProvider? provider) {
126 if (trade.walletId != walletId) {
127 return true;
128 }
129
130 final createdAt = trade.createdAt;
131 if (createdAt == null) {
132 printV('Skipping trade ${trade.id} because it has no createdAt');
133 return true;
134 }
135
136 if (DateTime.now().difference(createdAt).inHours > _maxTradeAgeHours) {
137 printV('Skipping trade ${trade.id} because it\'s older than ${_maxTradeAgeHours} hours');
138 return true;
139 }
140
141 if (_isFinalState(trade.state)) {
142 return true;
143 }
144
145 if (provider == null) {
146 printV('Skipping trade ${trade.id} because the provider is not supported');
147 return true;
148 }
149
150 if (appStore.settingsStore.exchangeStatus == ExchangeApiMode.torOnly &&
151 !provider.supportsOnionAddress) {
152 printV('Skipping ${provider.description}, no TOR support');
153 return true;
154 }
155
156 return false;
157 }
158
159 void _startTradeMonitoring(Trade trade, ExchangeProvider provider) {
160 final timer = Timer.periodic(
161 Duration(minutes: _tradeCheckIntervalMinutes),
162 (_) => _checkTradeStatus(trade, provider),
163 );
164
165 _checkTradeStatus(trade, provider);
166
167 _tradeTimers[trade.id] = timer;
168 }
169
170 Future<void> _checkTradeStatus(Trade trade, ExchangeProvider provider) async {
171 final lastUpdatedAtFromPrefs = preferences.getString('trade_${trade.id}_updated_at');
172
173 if (lastUpdatedAtFromPrefs != null) {
174 final lastUpdatedAtDateTime = DateTime.parse(lastUpdatedAtFromPrefs);
175 final timeSinceLastUpdate = DateTime.now().difference(lastUpdatedAtDateTime).inMinutes;
176
177 if (timeSinceLastUpdate < _tradeCheckIntervalMinutes) {
178 printV(
179 'Skipping trade ${trade.id} status update check because it was updated less than ${_tradeCheckIntervalMinutes} minutes ago ($timeSinceLastUpdate minutes ago)',
180 );
181 return;
182 }
183 }
184
185 try {
186 final updated = await provider.findTradeById(id: trade.id);
187 trade.mergeFindTradeByIdResult(updated);
188 printV('Trade ${trade.id} updated: ${trade.state}');
189 await trade.save();
190
191 await preferences.setString('trade_${trade.id}_updated_at', DateTime.now().toIso8601String());
192 printV('Trade ${trade.id} updated at: ${DateTime.now().toIso8601String()}');
193
194 // If the updated trade is in a final state, we cancel the timer
195 if (_isFinalState(updated.state)) {
196 printV('Trade ${trade.id} is in final state');
197 _cancelSingleTradeTimer(trade.id);
198 }
199 } catch (e) {
200 printV('Error fetching status for ${trade.id}: $e');
201 }
202 }
203
204 bool _isFinalState(TradeState state) {
205 return {
206 TradeState.completed.raw,
207 TradeState.success.raw,
208 TradeState.confirmed.raw,
209 TradeState.settled.raw,
210 TradeState.finished.raw,
211 TradeState.expired.raw,
212 TradeState.failed.raw,
213 TradeState.notFound.raw,
214 }.contains(state.raw);
215 }
216
217 void _cancelSingleTradeTimer(String tradeId) {
218 if (_tradeTimers.containsKey(tradeId)) {
219 _tradeTimers[tradeId]?.cancel();
220 _tradeTimers.remove(tradeId);
221 printV('Trade timer for ${tradeId} cancelled');
222 }
223 }
224
225 void _cancelMultipleTradeTimers(List<String> tradeIds) {
226 for (final tradeId in tradeIds) {
227 _cancelSingleTradeTimer(tradeId);
228 }
229 }
230
231 /// This is called when the app is brought back to foreground.
232 void resumeTradeMonitoring() {
233 if (appStore.wallet != null) {
234 monitorActiveTrades(appStore.wallet!.id);
235 }
236 }
237
238 /// There's no need to run the trade checks when the app is in background.
239 /// We only want to update the trade status when the app is in foreground.
240 /// This helps to reduce the battery usage, network usage and enhance overall privacy.
241 ///
242 /// This is called when the app is sent to background or when the app is closed.
243 void stopTradeMonitoring() {
244 printV('Stopping trade monitoring');
245 _cancelMultipleTradeTimers(_tradeTimers.keys.toList());
246 }
247 }