1
+import 'dart:async';
2
+import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart';
3
+import 'package:cake_wallet/exchange/trade.dart';
4
+import 'package:cake_wallet/exchange/trade_state.dart';
5
+import 'package:cake_wallet/store/dashboard/trades_store.dart';
6
+import 'package:cake_wallet/entities/exchange_api_mode.dart';
7
+import 'package:cake_wallet/exchange/exchange_provider_description.dart';
8
+import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
9
+import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
10
+import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
11
+import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
12
+import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart';
13
+import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart';
14
+import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
15
+import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
16
+import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
17
+import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
18
+import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart';
19
+import 'package:cw_core/utils/print_verbose.dart';
20
+import 'package:hive/hive.dart';
21
+import 'package:cake_wallet/store/app_store.dart';
22
+import 'package:shared_preferences/shared_preferences.dart';
23
+
24
+class TradeMonitor {
25
+ static const int _tradeCheckIntervalMinutes = 5;
26
+ static const int _maxTradeAgeHours = 24;
27
+
28
+ TradeMonitor({
29
+ required this.tradesStore,
30
+ required this.trades,
31
+ required this.appStore,
32
+ required this.preferences,
33
+ });
34
+
35
+ final TradesStore tradesStore;
36
+ final Box<Trade> trades;
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(tradesStore: trades);
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(tradesStore: trades);
63
+ case ExchangeProviderDescription.xoSwap:
64
+ return XOSwapExchangeProvider();
65
+ }
66
+ return null;
67
+ }
68
+
69
+ void monitorActiveTrades(String walletId) {
70
+ // Checks if the trade monitoring is permitted
71
+ // i.e the user has not disabled the exchange api mode or the status updates
72
+ final isTradeMonitoringPermitted = _isTradeMonitoringPermitted();
73
+ if (!isTradeMonitoringPermitted) {
74
+ return;
75
+ }
76
+
77
+ final trades = tradesStore.trades;
78
+ final tradesToCancel = <String>[];
79
+
80
+ for (final item in trades) {
81
+ final trade = item.trade;
82
+
83
+ final provider = _getProviderByDescription(trade.provider);
84
+
85
+ // Multiple checks to see if to skip the trade, if yes, we cancel the timer if it exists
86
+ if (_shouldSkipTrade(trade, walletId, provider)) {
87
+ tradesToCancel.add(trade.id);
88
+ continue;
89
+ }
90
+
91
+ if (_tradeTimers.containsKey(trade.id)) {
92
+ printV('Trade ${trade.id} is already being monitored');
93
+ continue;
94
+ } else {
95
+ _startTradeMonitoring(trade, provider!);
96
+ }
97
+ }
98
+
99
+ // After going through the list of available trades, we cancel the timers in the tradesToCancel list
100
+ _cancelMultipleTradeTimers(tradesToCancel);
101
+ }
102
+
103
+ bool _isTradeMonitoringPermitted() {
104
+ final disableAutomaticExchangeStatusUpdates =
105
+ appStore.settingsStore.disableAutomaticExchangeStatusUpdates;
106
+ if (disableAutomaticExchangeStatusUpdates) {
107
+ printV('Automatic exchange status updates are disabled');
108
+ return false;
109
+ }
110
+
111
+ final exchangeApiMode = appStore.settingsStore.exchangeStatus;
112
+ if (exchangeApiMode == ExchangeApiMode.disabled) {
113
+ printV('Exchange API mode is disabled');
114
+ return false;
115
+ }
116
+
117
+ return true;
118
+ }
119
+
120
+ bool _shouldSkipTrade(Trade trade, String walletId, ExchangeProvider? provider) {
121
+ if (trade.walletId != walletId) {
122
+ printV('Skipping trade ${trade.id} because it\'s not for this wallet');
123
+ return true;
124
+ }
125
+
126
+ final createdAt = trade.createdAt;
127
+ if (createdAt == null) {
128
+ printV('Skipping trade ${trade.id} because it has no createdAt');
129
+ return true;
130
+ }
131
+
132
+ if (DateTime.now().difference(createdAt).inHours > _maxTradeAgeHours) {
133
+ printV('Skipping trade ${trade.id} because it\'s older than ${_maxTradeAgeHours} hours');
134
+ return true;
135
+ }
136
+
137
+ if (_isFinalState(trade.state)) {
138
+ printV('Skipping trade ${trade.id} because it\'s in a final state');
139
+ return true;
140
+ }
141
+
142
+ if (provider == null) {
143
+ printV('Skipping trade ${trade.id} because the provider is not supported');
144
+ return true;
145
+ }
146
+
147
+ if (appStore.settingsStore.exchangeStatus == ExchangeApiMode.torOnly &&
148
+ !provider.supportsOnionAddress) {
149
+ printV('Skipping ${provider.description}, no TOR support');
150
+ return true;
151
+ }
152
+
153
+ return false;
154
+ }
155
+
156
+ void _startTradeMonitoring(Trade trade, ExchangeProvider provider) {
157
+ final timer = Timer.periodic(
158
+ Duration(minutes: _tradeCheckIntervalMinutes),
159
+ (_) => _checkTradeStatus(trade, provider),
160
+ );
161
+
162
+ _checkTradeStatus(trade, provider);
163
+
164
+ _tradeTimers[trade.id] = timer;
165
+ }
166
+
167
+ Future<void> _checkTradeStatus(Trade trade, ExchangeProvider provider) async {
168
+ final lastUpdatedAtFromPrefs = preferences.getString('trade_${trade.id}_updated_at');
169
+
170
+ if (lastUpdatedAtFromPrefs != null) {
171
+ final lastUpdatedAtDateTime = DateTime.parse(lastUpdatedAtFromPrefs);
172
+ final timeSinceLastUpdate = DateTime.now().difference(lastUpdatedAtDateTime).inMinutes;
173
+
174
+ if (timeSinceLastUpdate < _tradeCheckIntervalMinutes) {
175
+ printV(
176
+ 'Skipping trade ${trade.id} status update check because it was updated less than ${_tradeCheckIntervalMinutes} minutes ago ($timeSinceLastUpdate minutes ago)',
177
+ );
178
+ return;
179
+ }
180
+ }
181
+
182
+ try {
183
+ final updated = await provider.findTradeById(id: trade.id);
184
+ trade
185
+ ..stateRaw = updated.state.raw
186
+ ..receiveAmount = updated.receiveAmount ?? trade.receiveAmount
187
+ ..outputTransaction = updated.outputTransaction ?? trade.outputTransaction;
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
+}