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/store/settings_store.dart';
7
+import 'package:cake_wallet/entities/exchange_api_mode.dart';
8
+import 'package:cake_wallet/exchange/exchange_provider_description.dart';
9
+import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
10
+import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
11
+import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
12
+import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart';
13
+import 'package:cake_wallet/exchange/provider/letsexchange_exchange_provider.dart';
14
+import 'package:cake_wallet/exchange/provider/swaptrade_exchange_provider.dart';
15
+import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart';
16
+import 'package:cake_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
17
+import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
18
+import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
19
+import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart';
20
+import 'package:cw_core/utils/print_verbose.dart';
21
+import 'package:hive/hive.dart';
22
+
23
+class TradeMonitor {
24
+ static const int _tradeCheckIntervalMinutes = 1;
25
+ static const int _maxTradeAgeHours = 24;
26
+
27
+ TradeMonitor({
28
+ required this.tradesStore,
29
+ required this.settingsStore,
30
+ required this.trades,
31
+ });
32
+
33
+ final TradesStore tradesStore;
34
+ final Box<Trade> trades;
35
+ final SettingsStore settingsStore;
36
+ final Map<String, Timer> _tradeTimers = {};
37
+
38
+ ExchangeProvider? _getProviderByDescription(ExchangeProviderDescription description) {
39
+ switch (description) {
40
+ case ExchangeProviderDescription.changeNow:
41
+ return ChangeNowExchangeProvider(settingsStore: settingsStore);
42
+ case ExchangeProviderDescription.sideShift:
43
+ return SideShiftExchangeProvider();
44
+ case ExchangeProviderDescription.simpleSwap:
45
+ return SimpleSwapExchangeProvider();
46
+ case ExchangeProviderDescription.trocador:
47
+ return TrocadorExchangeProvider();
48
+ case ExchangeProviderDescription.exolix:
49
+ return ExolixExchangeProvider();
50
+ case ExchangeProviderDescription.thorChain:
51
+ return ThorChainExchangeProvider(tradesStore: trades);
52
+ case ExchangeProviderDescription.swapTrade:
53
+ return SwapTradeExchangeProvider();
54
+ case ExchangeProviderDescription.letsExchange:
55
+ return LetsExchangeExchangeProvider();
56
+ case ExchangeProviderDescription.stealthEx:
57
+ return StealthExExchangeProvider();
58
+ case ExchangeProviderDescription.chainflip:
59
+ return ChainflipExchangeProvider(tradesStore: trades);
60
+ case ExchangeProviderDescription.xoSwap:
61
+ return XOSwapExchangeProvider();
62
+ }
63
+ return null;
64
+ }
65
+
66
+ void monitorActiveTrades(String walletId) {
67
+ final now = DateTime.now();
68
+ final trades = tradesStore.trades;
69
+ final tradesToCancel = <String>[];
70
+
71
+ for (final item in trades) {
72
+ final trade = item.trade;
73
+
74
+ if (_shouldCancelTradeTimer(trade, walletId, now)) {
75
+ if (_tradeTimers.containsKey(trade.id)) {
76
+ tradesToCancel.add(trade.id);
77
+ }
78
+ continue;
79
+ }
80
+
81
+ if (!_tradeTimers.containsKey(trade.id)) {
82
+ printV('Starting trade monitoring for ${trade.id}');
83
+ _startTradeMonitoring(trade);
84
+ }
85
+ }
86
+
87
+ _cancelTradeTimers(tradesToCancel);
88
+ }
89
+
90
+ bool _shouldCancelTradeTimer(Trade trade, String walletId, DateTime now) {
91
+ if (trade.walletId != walletId) return true;
92
+
93
+ final createdAt = trade.createdAt;
94
+ if (createdAt == null) return true;
95
+
96
+ if (now.difference(createdAt).inHours > _maxTradeAgeHours) return true;
97
+
98
+ return _isFinalState(trade.state);
99
+ }
100
+
101
+ void _startTradeMonitoring(Trade trade) {
102
+ if (_tradeTimers.containsKey(trade.id)) return;
103
+
104
+ _checkTradeStatus(trade);
105
+
106
+ final timer = Timer.periodic(
107
+ Duration(minutes: _tradeCheckIntervalMinutes),
108
+ (_) => _checkTradeStatus(trade)
109
+ );
110
+
111
+ _tradeTimers[trade.id] = timer;
112
+ }
113
+
114
+ Future<void> _checkTradeStatus(Trade trade) async {
115
+ printV('Checking trade status for ${trade.id}');
116
+
117
+ if (_isTradeOld(trade)) {
118
+ printV('The trade ${trade.id} is older than $_maxTradeAgeHours hours, we will cancel the timer');
119
+ _cancelTradeTimer(trade.id);
120
+ return;
121
+ }
122
+
123
+ final provider = _getProviderByDescription(trade.provider);
124
+ if (provider == null) {
125
+ printV('No provider found for trade ${trade.id}');
126
+ return;
127
+ }
128
+
129
+ if (!_isExchangeModeEnabled(provider)) {
130
+ return;
131
+ }
132
+
133
+ try {
134
+ await _updateTradeStatus(trade, provider);
135
+ } catch (e) {
136
+ printV('Error fetching status for ${trade.id}: $e');
137
+ }
138
+ }
139
+
140
+ bool _isTradeOld(Trade trade) {
141
+ final now = DateTime.now();
142
+ final createdAt = trade.createdAt;
143
+ return createdAt != null && now.difference(createdAt).inHours > _maxTradeAgeHours;
144
+ }
145
+
146
+ bool _isExchangeModeEnabled(ExchangeProvider provider) {
147
+ final exchangeApiMode = settingsStore.exchangeStatus;
148
+
149
+ if (exchangeApiMode == ExchangeApiMode.disabled) {
150
+ printV('Exchange API mode is disabled');
151
+ return false;
152
+ }
153
+
154
+ if (exchangeApiMode == ExchangeApiMode.torOnly && !provider.supportsOnionAddress) {
155
+ printV('Skipping ${provider.description}, no TOR support');
156
+ return false;
157
+ }
158
+
159
+ return true;
160
+ }
161
+
162
+ Future<void> _updateTradeStatus(Trade trade, ExchangeProvider provider) async {
163
+ final updated = await provider.findTradeById(id: trade.id);
164
+ trade
165
+ ..stateRaw = updated.state.raw
166
+ ..receiveAmount = updated.receiveAmount ?? trade.receiveAmount
167
+ ..outputTransaction = updated.outputTransaction ?? trade.outputTransaction;
168
+ printV('Trade ${trade.id} updated: ${trade.state}');
169
+ await trade.save();
170
+
171
+ if (_isFinalState(updated.state)) {
172
+ printV('Trade ${trade.id} is in final state');
173
+ _cancelTradeTimer(trade.id);
174
+ }
175
+ }
176
+
177
+ void _cancelTradeTimer(String tradeId) {
178
+ _tradeTimers[tradeId]?.cancel();
179
+ _tradeTimers.remove(tradeId);
180
+ }
181
+
182
+ void _cancelTradeTimers(List<String> tradeIds) {
183
+ for (final tradeId in tradeIds) {
184
+ _cancelTradeTimer(tradeId);
185
+ }
186
+ }
187
+
188
+ void cancelAllTradeTimers() {
189
+ for (final timer in _tradeTimers.values) {
190
+ timer.cancel();
191
+ }
192
+ _tradeTimers.clear();
193
+ }
194
+
195
+ bool _isFinalState(TradeState state) {
196
+ return {
197
+ TradeState.completed.raw,
198
+ TradeState.success.raw,
199
+ TradeState.confirmed.raw,
200
+ TradeState.settled.raw,
201
+ TradeState.finished.raw,
202
+ TradeState.expired.raw,
203
+ TradeState.failed.raw,
204
+ TradeState.notFound.raw,
205
+ }.contains(state.raw);
206
+ }
207
+
208
+ void dispose() {
209
+ cancelAllTradeTimers();
210
+ }
211
+}